Merge branch 'develop' into feature/2024.9.0
# Conflicts: # locales/en-US.yml # locales/ja-JP.yml # packages/backend/src/core/NoteCreateService.ts # packages/backend/src/core/NoteDeleteService.ts # packages/backend/src/core/NoteEditService.ts # packages/frontend-shared/js/config.ts # packages/frontend/src/boot/common.ts # packages/frontend/src/pages/following-feed.vue # packages/misskey-js/src/autogen/endpoint.ts
This commit is contained in:
commit
8a34d8e9d2
52 changed files with 1073 additions and 268 deletions
1
packages/frontend/@types/global.d.ts
vendored
1
packages/frontend/@types/global.d.ts
vendored
|
|
@ -6,6 +6,7 @@
|
|||
type FIXME = any;
|
||||
|
||||
declare const _LANGS_: string[][];
|
||||
declare const _LANGS_VERSION_: string;
|
||||
declare const _VERSION_: string;
|
||||
declare const _ENV_: string;
|
||||
declare const _DEV_: boolean;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
import { computed, watch, version as vueVersion, App } from 'vue';
|
||||
import { compareVersions } from 'compare-versions';
|
||||
import { version, lang, updateLocale, locale } from '@@/js/config.js';
|
||||
import { version, lang, langsVersion, updateLocale, locale } from '@@/js/config.js';
|
||||
import widgets from '@/widgets/index.js';
|
||||
import directives from '@/directives/index.js';
|
||||
import components from '@/components/index.js';
|
||||
|
|
@ -81,14 +81,15 @@ export async function common(createVue: () => App<Element>) {
|
|||
|
||||
//#region Detect language & fetch translations
|
||||
const localeVersion = miLocalStorage.getItem('localeVersion');
|
||||
const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null);
|
||||
const localeOutdated = (localeVersion == null || localeVersion !== langsVersion || locale == null);
|
||||
if (localeOutdated) {
|
||||
const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
|
||||
console.info(`Updating locales from version ${localeVersion ?? 'N/A'} to ${langsVersion}`);
|
||||
const res = await window.fetch(`/assets/locales/${lang}.${langsVersion}.json`);
|
||||
if (res.status === 200) {
|
||||
const newLocale = await res.text();
|
||||
const parsedNewLocale = JSON.parse(newLocale);
|
||||
miLocalStorage.setItem('locale', newLocale);
|
||||
miLocalStorage.setItem('localeVersion', version);
|
||||
miLocalStorage.setItem('localeVersion', langsVersion);
|
||||
updateLocale(parsedNewLocale);
|
||||
updateI18n(parsedNewLocale);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,16 +24,14 @@ import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
|
|||
import { Paging } from '@/components/MkPagination.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
const props = defineProps<{
|
||||
userId: string;
|
||||
withRenotes?: boolean;
|
||||
withReplies?: boolean;
|
||||
onlyFiles?: boolean;
|
||||
}>(), {
|
||||
withRenotes: false,
|
||||
withReplies: true,
|
||||
onlyFiles: false,
|
||||
});
|
||||
withNonPublic: boolean;
|
||||
withQuotes: boolean;
|
||||
withReplies: boolean;
|
||||
withBots: boolean;
|
||||
onlyFiles: boolean;
|
||||
}>();
|
||||
|
||||
const loadError: Ref<string | null> = ref(null);
|
||||
const user: Ref<Misskey.entities.UserDetailed | null> = ref(null);
|
||||
|
|
@ -43,9 +41,13 @@ const pagination: Paging<'users/notes'> = {
|
|||
limit: 10,
|
||||
params: computed(() => ({
|
||||
userId: props.userId,
|
||||
withRenotes: props.withRenotes,
|
||||
withNonPublic: props.withNonPublic,
|
||||
withRenotes: false,
|
||||
withQuotes: props.withQuotes,
|
||||
withReplies: props.withReplies,
|
||||
withRepliesToSelf: props.withReplies,
|
||||
withFiles: props.onlyFiles,
|
||||
allowPartial: true,
|
||||
})),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export const navbarItemDef = reactive({
|
|||
followRequests: {
|
||||
title: i18n.ts.followRequests,
|
||||
icon: 'ti ti-user-plus',
|
||||
show: computed(() => $i != null && $i.isLocked),
|
||||
show: computed(() => $i != null && ($i.isLocked || $i.hasPendingReceivedFollowRequest || $i.hasPendingSentFollowRequest)),
|
||||
indicated: computed(() => $i != null && $i.hasPendingReceivedFollowRequest),
|
||||
to: '/my/follow-requests',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -5,39 +5,43 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader/></template>
|
||||
<template #header><MkPageHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.noFollowRequests }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{items}">
|
||||
<div class="mk-follow-requests">
|
||||
<div v-for="req in items" :key="req.id" class="user _panel">
|
||||
<MkAvatar class="avatar" :user="req.follower" indicator link preview/>
|
||||
<div class="body">
|
||||
<div class="name">
|
||||
<MkA v-user-preview="req.follower.id" class="name" :to="userPage(req.follower)"><MkUserName :user="req.follower"/></MkA>
|
||||
<p class="acct">@{{ acct(req.follower) }}</p>
|
||||
</div>
|
||||
<div class="commands">
|
||||
<MkButton class="command" rounded primary @click="accept(req.follower)"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
|
||||
<MkButton class="command" rounded danger @click="reject(req.follower)"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
|
||||
<MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs">
|
||||
<div :key="tab" class="_gaps">
|
||||
<MkPagination ref="paginationComponent" :pagination="pagination">
|
||||
<template #empty>
|
||||
<div class="_fullinfo">
|
||||
<img :src="infoImageUrl" class="_ghost"/>
|
||||
<div>{{ i18n.ts.noFollowRequests }}</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #default="{items}">
|
||||
<div class="mk-follow-requests">
|
||||
<div v-for="req in items" :key="req.id" class="user _panel">
|
||||
<MkAvatar class="avatar" :user="displayUser(req)" indicator link preview/>
|
||||
<div class="body">
|
||||
<div class="name">
|
||||
<MkA v-user-preview="displayUser(req).id" class="name" :to="userPage(displayUser(req))"><MkUserName :user="displayUser(req)"/></MkA>
|
||||
<p class="acct">@{{ acct(displayUser(req)) }}</p>
|
||||
</div>
|
||||
<div v-if="tab === 'list'" class="commands">
|
||||
<MkButton class="command" rounded primary @click="accept(displayUser(req))"><i class="ti ti-check"/> {{ i18n.ts.accept }}</MkButton>
|
||||
<MkButton class="command" rounded danger @click="reject(displayUser(req))"><i class="ti ti-x"/> {{ i18n.ts.reject }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</template>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkHorizontalSwipe>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { shallowRef, computed } from 'vue';
|
||||
import { shallowRef, computed, ref } from 'vue';
|
||||
import MkPagination from '@/components/MkPagination.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { userPage, acct } from '@/filters/user.js';
|
||||
|
|
@ -45,29 +49,53 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
|
|||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import { infoImageUrl } from '@/instance.js';
|
||||
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'following/requests/list' as const,
|
||||
limit: 10,
|
||||
};
|
||||
const pagination = computed(() => tab.value === 'list'
|
||||
? {
|
||||
endpoint: 'following/requests/list' as const,
|
||||
limit: 10,
|
||||
}
|
||||
: {
|
||||
endpoint: 'following/requests/sent' as const,
|
||||
limit: 10,
|
||||
},
|
||||
);
|
||||
|
||||
function accept(user) {
|
||||
misskeyApi('following/requests/accept', { userId: user.id }).then(() => {
|
||||
paginationComponent.value.reload();
|
||||
paginationComponent.value?.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function reject(user) {
|
||||
misskeyApi('following/requests/reject', { userId: user.id }).then(() => {
|
||||
paginationComponent.value.reload();
|
||||
paginationComponent.value?.reload();
|
||||
});
|
||||
}
|
||||
|
||||
function displayUser(req) {
|
||||
return tab.value === 'list' ? req.follower : req.followee;
|
||||
}
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
const headerTabs = computed(() => [
|
||||
{
|
||||
key: 'list',
|
||||
title: i18n.ts.followRequests,
|
||||
icon: 'ph-envelope ph-bold ph-lg',
|
||||
}, {
|
||||
key: 'sent',
|
||||
title: i18n.ts.pendingFollowRequests,
|
||||
icon: 'ph-paper-plane-tilt ph-bold ph-lg',
|
||||
},
|
||||
]);
|
||||
|
||||
const tab = ref($i?.isLocked || !$i.hasPendingSentFollowRequest ? 'list' : 'sent');
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.followRequests,
|
||||
|
|
|
|||
|
|
@ -30,18 +30,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<div v-if="isWideViewport" ref="userScroll" :class="$style.user">
|
||||
<MkHorizontalSwipe v-if="selectedUserId" v-model:tab="currentTab" :tabs="headerTabs">
|
||||
<SkUserRecentNotes ref="userRecentNotes" :userId="selectedUserId" :withRenotes="withUserRenotes" :withReplies="withUserReplies" :onlyFiles="withOnlyFiles"/>
|
||||
<SkUserRecentNotes ref="userRecentNotes" :userId="selectedUserId" :withNonPublic="withNonPublic" :withQuotes="withQuotes" :withBots="withBots" :withReplies="withReplies" :onlyFiles="onlyFiles"/>
|
||||
</MkHorizontalSwipe>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export type FollowingFeedTab = typeof followingTab | typeof mutualsTab;
|
||||
export const followingTab = 'following' as const;
|
||||
export const mutualsTab = 'mutuals' as const;
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, Ref, ref, shallowRef } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
|
|
@ -63,20 +57,49 @@ import { checkWordMute } from '@/scripts/check-word-mute.js';
|
|||
import SkUserRecentNotes from '@/components/SkUserRecentNotes.vue';
|
||||
import { useScrollPositionManager } from '@/nirax.js';
|
||||
import { getScrollContainer } from '@@/js/scroll.js';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { deepMerge } from '@/scripts/merge.js';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
initialTab?: FollowingFeedTab,
|
||||
}>(), {
|
||||
initialTab: followingTab,
|
||||
const withNonPublic = computed({
|
||||
get: () => defaultStore.reactiveState.followingFeed.value.withNonPublic,
|
||||
set: value => saveFollowingFilter('withNonPublic', value),
|
||||
});
|
||||
const withQuotes = computed({
|
||||
get: () => defaultStore.reactiveState.followingFeed.value.withQuotes,
|
||||
set: value => saveFollowingFilter('withQuotes', value),
|
||||
});
|
||||
const withBots = computed({
|
||||
get: () => defaultStore.reactiveState.followingFeed.value.withBots,
|
||||
set: value => saveFollowingFilter('withBots', value),
|
||||
});
|
||||
const withReplies = computed({
|
||||
get: () => defaultStore.reactiveState.followingFeed.value.withReplies,
|
||||
set: value => saveFollowingFilter('withReplies', value),
|
||||
});
|
||||
const onlyFiles = computed({
|
||||
get: () => defaultStore.reactiveState.followingFeed.value.onlyFiles,
|
||||
set: value => saveFollowingFilter('onlyFiles', value),
|
||||
});
|
||||
const onlyMutuals = computed({
|
||||
get: () => defaultStore.reactiveState.followingFeed.value.onlyMutuals,
|
||||
set: value => saveFollowingFilter('onlyMutuals', value),
|
||||
});
|
||||
|
||||
// Based on timeline.saveTlFilter()
|
||||
function saveFollowingFilter(key: keyof typeof defaultStore.state.followingFeed, value: boolean) {
|
||||
const out = deepMerge({ [key]: value }, defaultStore.state.followingFeed);
|
||||
defaultStore.set('followingFeed', out);
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// Vue complains, but we *want* to lose reactivity here.
|
||||
// Otherwise, the user would be unable to change the tab.
|
||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
|
||||
const currentTab: Ref<FollowingFeedTab> = ref(props.initialTab);
|
||||
const mutualsOnly: Ref<boolean> = computed(() => currentTab.value === mutualsTab);
|
||||
const followingTab = 'following' as const;
|
||||
const mutualsTab = 'mutuals' as const;
|
||||
const currentTab = computed({
|
||||
get: () => onlyMutuals.value ? mutualsTab : followingTab,
|
||||
set: value => onlyMutuals.value = (value === mutualsTab),
|
||||
});
|
||||
|
||||
const userRecentNotes = shallowRef<InstanceType<typeof SkUserRecentNotes>>();
|
||||
const userScroll = shallowRef<HTMLElement>();
|
||||
const noteScroll = shallowRef<HTMLElement>();
|
||||
|
|
@ -161,55 +184,60 @@ const latestNotesPagination: Paging<'notes/following'> = {
|
|||
endpoint: 'notes/following' as const,
|
||||
limit: 20,
|
||||
params: computed(() => ({
|
||||
mutualsOnly: mutualsOnly.value,
|
||||
mutualsOnly: onlyMutuals.value,
|
||||
filesOnly: onlyFiles.value,
|
||||
includeNonPublic: withNonPublic.value,
|
||||
includeReplies: withReplies.value,
|
||||
includeQuotes: withQuotes.value,
|
||||
includeBots: withBots.value,
|
||||
})),
|
||||
};
|
||||
|
||||
const withUserRenotes = ref(false);
|
||||
const withUserReplies = ref(true);
|
||||
const withOnlyFiles = ref(false);
|
||||
|
||||
const headerActions = computed(() => {
|
||||
const actions: PageHeaderItem[] = [
|
||||
{
|
||||
icon: 'ti ti-refresh',
|
||||
text: i18n.ts.reload,
|
||||
handler: () => reload(),
|
||||
const headerActions: PageHeaderItem[] = [
|
||||
{
|
||||
icon: 'ti ti-refresh',
|
||||
text: i18n.ts.reload,
|
||||
handler: () => reload(),
|
||||
},
|
||||
{
|
||||
icon: 'ti ti-dots',
|
||||
text: i18n.ts.options,
|
||||
handler: (ev) => {
|
||||
os.popupMenu([
|
||||
{
|
||||
type: 'switch',
|
||||
text: i18n.ts.showNonPublicNotes,
|
||||
ref: withNonPublic,
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
text: i18n.ts.showQuotes,
|
||||
ref: withQuotes,
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
text: i18n.ts.showBots,
|
||||
ref: withBots,
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
text: i18n.ts.showReplies,
|
||||
ref: withReplies,
|
||||
disabled: onlyFiles,
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
text: i18n.ts.fileAttachedOnly,
|
||||
ref: onlyFiles,
|
||||
disabled: withReplies,
|
||||
},
|
||||
], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
];
|
||||
|
||||
if (isWideViewport.value) {
|
||||
actions.push({
|
||||
icon: 'ti ti-dots',
|
||||
text: i18n.ts.options,
|
||||
handler: (ev) => {
|
||||
os.popupMenu([
|
||||
{
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRenotes,
|
||||
ref: withUserRenotes,
|
||||
}, {
|
||||
type: 'switch',
|
||||
text: i18n.ts.showRepliesToOthersInTimeline,
|
||||
ref: withUserReplies,
|
||||
disabled: withOnlyFiles,
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
type: 'switch',
|
||||
text: i18n.ts.fileAttachedOnly,
|
||||
ref: withOnlyFiles,
|
||||
disabled: withUserReplies,
|
||||
},
|
||||
], ev.currentTarget ?? ev.target);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return actions;
|
||||
});
|
||||
},
|
||||
];
|
||||
|
||||
const headerTabs = computed(() => [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -241,6 +241,17 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
where: 'deviceAccount',
|
||||
default: [] as Misskey.entities.UserList[],
|
||||
},
|
||||
followingFeed: {
|
||||
where: 'account',
|
||||
default: {
|
||||
withNonPublic: false,
|
||||
withQuotes: false,
|
||||
withBots: true,
|
||||
withReplies: false,
|
||||
onlyFiles: false,
|
||||
onlyMutuals: false,
|
||||
},
|
||||
},
|
||||
|
||||
overridedDeviceKind: {
|
||||
where: 'device',
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import path from 'path';
|
|||
import pluginReplace from '@rollup/plugin-replace';
|
||||
import pluginVue from '@vitejs/plugin-vue';
|
||||
import { type UserConfig, defineConfig } from 'vite';
|
||||
|
||||
import { localesVersion } from '../../locales/version.js';
|
||||
import locales from '../../locales/index.js';
|
||||
import meta from '../../package.json';
|
||||
import packageInfo from './package.json' with { type: 'json' };
|
||||
|
|
@ -114,6 +114,7 @@ export function getConfig(): UserConfig {
|
|||
define: {
|
||||
_VERSION_: JSON.stringify(meta.version),
|
||||
_LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])),
|
||||
_LANGS_VERSION_: JSON.stringify(localesVersion),
|
||||
_ENV_: JSON.stringify(process.env.NODE_ENV),
|
||||
_DEV_: process.env.NODE_ENV !== 'production',
|
||||
_PERF_PREFIX_: JSON.stringify('Misskey:'),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue