Merge remote-tracking branch 'refs/remotes/misskey-original/develop' into develop
# Conflicts: # packages/frontend/src/cache.ts # packages/frontend/src/pages/admin/index.vue # packages/frontend/src/pages/settings/general.vue # packages/frontend/src/pages/timeline.vue
This commit is contained in:
commit
07b4338eff
100 changed files with 1929 additions and 328 deletions
|
|
@ -42,7 +42,7 @@ import MkInput from '@/components/MkInput.vue';
|
|||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { lookupFile } from '@/scripts/admin-lookup.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
|
||||
|
|
@ -73,33 +73,10 @@ function clear() {
|
|||
});
|
||||
}
|
||||
|
||||
function show(file) {
|
||||
os.pageWindow(`/admin/file/${file.id}`);
|
||||
}
|
||||
|
||||
async function find() {
|
||||
const { canceled, result: q } = await os.inputText({
|
||||
title: i18n.ts.fileIdOrUrl,
|
||||
minLength: 1,
|
||||
});
|
||||
if (canceled) return;
|
||||
|
||||
misskeyApi('admin/drive/show-file', q.startsWith('http://') || q.startsWith('https://') ? { url: q.trim() } : { fileId: q.trim() }).then(file => {
|
||||
show(file);
|
||||
}).catch(err => {
|
||||
if (err.code === 'NO_SUCH_FILE') {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.notFound,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const headerActions = computed(() => [{
|
||||
text: i18n.ts.lookup,
|
||||
icon: 'ti ti-search',
|
||||
handler: find,
|
||||
handler: lookupFile,
|
||||
}, {
|
||||
text: i18n.ts.clearCachedFiles,
|
||||
icon: 'ti ti-trash',
|
||||
|
|
|
|||
|
|
@ -33,9 +33,10 @@ import { i18n } from '@/i18n.js';
|
|||
import MkSuperMenu from '@/components/MkSuperMenu.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import { instance } from '@/instance.js';
|
||||
import { lookup } from '@/scripts/lookup.js';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { lookupUser, lookupUserByEmail } from '@/scripts/lookup-user.js';
|
||||
import { lookupUser, lookupUserByEmail, lookupFile } from '@/scripts/admin-lookup.js';
|
||||
import { useRouter } from '@/router/supplier.js';
|
||||
import { PageMetadata, definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
|
||||
import { bannerDark, bannerLight, defaultStore, iconDark, iconLight } from '@/store.js';
|
||||
|
|
@ -96,7 +97,7 @@ const menuDef = computed(() => [{
|
|||
type: 'button',
|
||||
icon: 'ti ti-search',
|
||||
text: i18n.ts.lookup,
|
||||
action: lookup,
|
||||
action: adminLookup,
|
||||
}, ...(instance.disableRegistration ? [{
|
||||
type: 'button',
|
||||
icon: 'ti ti-user-plus',
|
||||
|
|
@ -296,7 +297,7 @@ function invite() {
|
|||
});
|
||||
}
|
||||
|
||||
function lookup(ev: MouseEvent) {
|
||||
function adminLookup(ev: MouseEvent) {
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.user,
|
||||
icon: 'ti ti-user',
|
||||
|
|
@ -309,23 +310,17 @@ function lookup(ev: MouseEvent) {
|
|||
action: () => {
|
||||
lookupUserByEmail();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.note,
|
||||
icon: 'ti ti-pencil',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.file,
|
||||
icon: 'ti ti-cloud',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
lookupFile();
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.instance,
|
||||
icon: 'ti ti-planet',
|
||||
text: i18n.ts.lookup,
|
||||
icon: 'ti ti-world-search',
|
||||
action: () => {
|
||||
alert('TODO');
|
||||
lookup();
|
||||
},
|
||||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ import MkInput from '@/components/MkInput.vue';
|
|||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { lookupUser } from '@/scripts/lookup-user.js';
|
||||
import { lookupUser } from '@/scripts/admin-lookup.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||
|
|
|
|||
142
packages/frontend/src/pages/announcement.vue
Normal file
142
packages/frontend/src/pages/announcement.vue
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<Transition
|
||||
:enterActiveClass="defaultStore.state.animation ? $style.fadeEnterActive : ''"
|
||||
:leaveActiveClass="defaultStore.state.animation ? $style.fadeLeaveActive : ''"
|
||||
:enterFromClass="defaultStore.state.animation ? $style.fadeEnterFrom : ''"
|
||||
:leaveToClass="defaultStore.state.animation ? $style.fadeLeaveTo : ''"
|
||||
mode="out-in"
|
||||
>
|
||||
<div v-if="announcement" :key="announcement.id" class="_panel" :class="$style.announcement">
|
||||
<div v-if="announcement.forYou" :class="$style.forYou"><i class="ti ti-pin"></i> {{ i18n.ts.forYou }}</div>
|
||||
<div :class="$style.header">
|
||||
<span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
|
||||
<span style="margin-right: 0.5em;">
|
||||
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
|
||||
<i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
|
||||
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
|
||||
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
|
||||
</span>
|
||||
<Mfm :text="announcement.title"/>
|
||||
</div>
|
||||
<div :class="$style.content">
|
||||
<Mfm :text="announcement.text"/>
|
||||
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
|
||||
<div style="margin-top: 8px; opacity: 0.7; font-size: 85%;">
|
||||
{{ i18n.ts.createdAt }}: <MkTime :time="announcement.createdAt" mode="detail"/>
|
||||
</div>
|
||||
<div v-if="announcement.updatedAt" style="opacity: 0.7; font-size: 85%;">
|
||||
{{ i18n.ts.updatedAt }}: <MkTime :time="announcement.updatedAt" mode="detail"/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="$i && !announcement.silence && !announcement.isRead" :class="$style.footer">
|
||||
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
<MkError v-else-if="error" @retry="fetch()"/>
|
||||
<MkLoading v-else/>
|
||||
</Transition>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { $i, updateAccount } from '@/account.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
||||
const props = defineProps<{
|
||||
announcementId: string;
|
||||
}>();
|
||||
|
||||
const announcement = ref<Misskey.entities.Announcement | null>(null);
|
||||
const error = ref<any>(null);
|
||||
const path = computed(() => props.announcementId);
|
||||
|
||||
function fetch() {
|
||||
announcement.value = null;
|
||||
misskeyApi('announcements/show', {
|
||||
announcementId: props.announcementId,
|
||||
}).then(async _announcement => {
|
||||
announcement.value = _announcement;
|
||||
}).catch(err => {
|
||||
error.value = err;
|
||||
});
|
||||
}
|
||||
|
||||
async function read(target: Misskey.entities.Announcement): Promise<void> {
|
||||
if (target.needConfirmationToRead) {
|
||||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts._announcement.readConfirmTitle,
|
||||
text: i18n.tsx._announcement.readConfirmText({ title: target.title }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
}
|
||||
|
||||
target.isRead = true;
|
||||
await misskeyApi('i/read-announcement', { announcementId: target.id });
|
||||
if ($i) {
|
||||
updateAccount({
|
||||
unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== target.id),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => path.value, fetch, { immediate: true });
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: announcement.value ? `${i18n.ts.announcements}: ${announcement.value.title}` : i18n.ts.announcements,
|
||||
icon: 'ti ti-speakerphone',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.announcement {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.forYou {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 24px;
|
||||
font-size: 90%;
|
||||
white-space: pre;
|
||||
color: #d28a3f;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 16px;
|
||||
font-weight: bold;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
.content {
|
||||
> img {
|
||||
display: block;
|
||||
max-height: 300px;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -21,14 +21,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
|
||||
<i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
|
||||
</span>
|
||||
<span>{{ announcement.title }}</span>
|
||||
<MkA :to="`/announcements/${announcement.id}`"><span>{{ announcement.title }}</span></MkA>
|
||||
</div>
|
||||
<div :class="$style.content">
|
||||
<Mfm :text="announcement.text"/>
|
||||
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
|
||||
<div style="opacity: 0.7; font-size: 85%;">
|
||||
<MkTime :time="announcement.updatedAt ?? announcement.createdAt" mode="detail"/>
|
||||
</div>
|
||||
<MkA :to="`/announcements/${announcement.id}`">
|
||||
<div style="margin-top: 8px; opacity: 0.7; font-size: 85%;">
|
||||
{{ i18n.ts.createdAt }}: <MkTime :time="announcement.createdAt" mode="detail"/>
|
||||
</div>
|
||||
<div v-if="announcement.updatedAt" style="opacity: 0.7; font-size: 85%;">
|
||||
{{ i18n.ts.updatedAt }}: <MkTime :time="announcement.updatedAt" mode="detail"/>
|
||||
</div>
|
||||
</MkA>
|
||||
</div>
|
||||
<div v-if="tab !== 'past' && $i && !announcement.silence && !announcement.isRead" :class="$style.footer">
|
||||
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton>
|
||||
|
|
@ -73,24 +78,24 @@ const paginationEl = ref<InstanceType<typeof MkPagination>>();
|
|||
|
||||
const tab = ref('current');
|
||||
|
||||
async function read(announcement) {
|
||||
if (announcement.needConfirmationToRead) {
|
||||
async function read(target) {
|
||||
if (target.needConfirmationToRead) {
|
||||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts._announcement.readConfirmTitle,
|
||||
text: i18n.tsx._announcement.readConfirmText({ title: announcement.title }),
|
||||
text: i18n.tsx._announcement.readConfirmText({ title: target.title }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
}
|
||||
|
||||
if (!paginationEl.value) return;
|
||||
paginationEl.value.updateItem(announcement.id, a => {
|
||||
paginationEl.value.updateItem(target.id, a => {
|
||||
a.isRead = true;
|
||||
return a;
|
||||
});
|
||||
misskeyApi('i/read-announcement', { announcementId: announcement.id });
|
||||
misskeyApi('i/read-announcement', { announcementId: target.id });
|
||||
updateAccount({
|
||||
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== announcement.id),
|
||||
unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import { url } from '@/config.js';
|
||||
import { favoritedChannelsCache } from '@/cache.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
|
|
@ -152,6 +153,7 @@ function favorite() {
|
|||
channelId: channel.value.id,
|
||||
}).then(() => {
|
||||
favorited.value = true;
|
||||
favoritedChannelsCache.delete();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -167,6 +169,7 @@ async function unfavorite() {
|
|||
channelId: channel.value.id,
|
||||
}).then(() => {
|
||||
favorited.value = false;
|
||||
favoritedChannelsCache.delete();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkSwitch v-model="localOnly">{{ i18n.ts.localOnly }}</MkSwitch>
|
||||
<MkSwitch v-model="caseSensitive">{{ i18n.ts.caseSensitive }}</MkSwitch>
|
||||
<MkSwitch v-model="withFile">{{ i18n.ts.withFileAntenna }}</MkSwitch>
|
||||
<MkSwitch v-model="notify">{{ i18n.ts.notifyAntenna }}</MkSwitch>
|
||||
</div>
|
||||
<div :class="$style.actions">
|
||||
<MkButton inline primary @click="saveAntenna()"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
|
||||
|
|
@ -82,7 +81,6 @@ const localOnly = ref<boolean>(props.antenna.localOnly);
|
|||
const excludeBots = ref<boolean>(props.antenna.excludeBots);
|
||||
const withReplies = ref<boolean>(props.antenna.withReplies);
|
||||
const withFile = ref<boolean>(props.antenna.withFile);
|
||||
const notify = ref<boolean>(props.antenna.notify);
|
||||
const userLists = ref<Misskey.entities.UserList[] | null>(null);
|
||||
|
||||
watch(() => src.value, async () => {
|
||||
|
|
@ -99,7 +97,6 @@ async function saveAntenna() {
|
|||
excludeBots: excludeBots.value,
|
||||
withReplies: withReplies.value,
|
||||
withFile: withFile.value,
|
||||
notify: notify.value,
|
||||
caseSensitive: caseSensitive.value,
|
||||
localOnly: localOnly.value,
|
||||
users: users.value.trim().split('\n').map(x => x.trim()),
|
||||
|
|
|
|||
|
|
@ -94,6 +94,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<div class="_gaps_m">
|
||||
<div class="_gaps_s">
|
||||
<MkSwitch v-model="collapseRenotes">
|
||||
<template #label>{{ i18n.ts.collapseRenotes }}</template>
|
||||
<template #caption>{{ i18n.ts.collapseRenotesDescription }}</template>
|
||||
</MkSwitch>
|
||||
<MkSwitch v-model="showNoteActionsOnlyHover">{{ i18n.ts.showNoteActionsOnlyHover }}</MkSwitch>
|
||||
<MkSwitch v-model="showClipButtonInNoteFooter">{{ i18n.ts.showClipButtonInNoteFooter }}</MkSwitch>
|
||||
<MkSwitch v-model="collapseRenotes">{{ i18n.ts.collapseRenotes }}</MkSwitch>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ import { i18n } from '@/i18n.js';
|
|||
import { instance } from '@/instance.js';
|
||||
import { $i } from '@/account.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { antennasCache, userFavoriteListsCache, userListsCache } from '@/cache.js';
|
||||
import { antennasCache, userFavoriteListsCache, userListsCache, favoritedChannelsCache } from '@/cache.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { deepMerge } from '@/scripts/merge.js';
|
||||
import { MenuItem } from '@/types/menu.js';
|
||||
|
|
@ -193,9 +193,7 @@ async function chooseAntenna(ev: MouseEvent): Promise<void> {
|
|||
}
|
||||
|
||||
async function chooseChannel(ev: MouseEvent): Promise<void> {
|
||||
const channels = await misskeyApi('channels/my-favorites', {
|
||||
limit: 100,
|
||||
});
|
||||
const channels = await favoritedChannelsCache.fetch();
|
||||
const items: MenuItem[] = [
|
||||
...channels.map(channel => {
|
||||
const lastReadedAt = miLocalStorage.getItemAsJson(`channelLastReadedAt:${channel.id}`) ?? null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue