diff --git a/locales/index.d.ts b/locales/index.d.ts index 588218cd00..04cd022ad4 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -9759,6 +9759,10 @@ export interface Locale extends ILocale { * ロールタイムライン */ "roleTimeline": string; + /** + * Following + */ + "following": string; }; }; "_dialog": { @@ -11504,6 +11508,10 @@ export interface Locale extends ILocale { * Remote followers may have incomplete or outdated activity */ "remoteFollowersWarning": string; + /** + * Select a follow relationship... + */ + "selectFollowRelationship": string; } declare const locales: { [lang: string]: Locale; diff --git a/package.json b/package.json index 48dccc6895..cad49748a4 100644 --- a/package.json +++ b/package.json @@ -66,13 +66,15 @@ "esbuild": "0.24.0", "glob": "11.0.0" }, + "optionalDependencies": { + "cypress": "13.15.2" + }, "devDependencies": { "@misskey-dev/eslint-plugin": "2.0.3", "@types/node": "22.9.0", "@typescript-eslint/eslint-plugin": "7.17.0", "@typescript-eslint/parser": "7.17.0", "cross-env": "7.0.3", - "cypress": "13.15.2", "eslint": "9.14.0", "globals": "15.12.0", "ncp": "2.0.0", diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index d1ae13d706..87b2b620d7 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -32,7 +32,7 @@ import { AbuseReportService } from '@/core/AbuseReportService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { fromTuple } from '@/misc/from-tuple.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; -import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; +import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isApObject, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { ApNoteService } from './models/ApNoteService.js'; import { ApLoggerService } from './ApLoggerService.js'; import { ApDbResolverService } from './ApDbResolverService.js'; @@ -166,7 +166,7 @@ export class ApInboxService { } else if (isAnnounce(activity)) { return await this.announce(actor, activity, resolver); } else if (isLike(activity)) { - return await this.like(actor, activity); + return await this.like(actor, activity, resolver); } else if (isUndo(activity)) { return await this.undo(actor, activity, resolver); } else if (isBlock(activity)) { @@ -198,10 +198,13 @@ export class ApInboxService { } @bindThis - private async like(actor: MiRemoteUser, activity: ILike): Promise { + private async like(actor: MiRemoteUser, activity: ILike, resolver?: Resolver): Promise { const targetUri = getApId(activity.object); - const note = await this.apNoteService.fetchNote(targetUri); + const object = fromTuple(activity.object); + if (!object) return 'skip: activity has no object property'; + + const note = await this.apNoteService.resolveNote(object, { resolver }); if (!note) return `skip: target note not found ${targetUri}`; await this.apNoteService.extractEmojis(activity.tag ?? [], actor.host).catch(() => null); @@ -272,8 +275,12 @@ export class ApInboxService { } if (activity.target === actor.featured) { - const object = fromTuple(activity.object); - const note = await this.apNoteService.resolveNote(object, { resolver }); + const activityObject = fromTuple(activity.object); + if (isApObject(activityObject) && !isPost(activityObject)) { + return `unsupported featured object type: ${getApType(activityObject)}`; + } + + const note = await this.apNoteService.resolveNote(activityObject, { resolver }); if (note == null) return 'note not found'; await this.notePiningService.addPinned(actor, note.id); return; @@ -386,7 +393,7 @@ export class ApInboxService { } @bindThis - private async create(actor: MiRemoteUser, activity: ICreate, resolver?: Resolver): Promise { + private async create(actor: MiRemoteUser, activity: ICreate | IUpdate, resolver?: Resolver): Promise { const uri = getApId(activity); this.logger.info(`Create: ${uri}`); @@ -421,14 +428,14 @@ export class ApInboxService { }); if (isPost(object)) { - await this.createNote(resolver, actor, object, false, activity); + await this.createNote(resolver, actor, object, false); } else { return `Unknown type: ${getApType(object)}`; } } @bindThis - private async createNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false, activity?: ICreate): Promise { + private async createNote(resolver: Resolver, actor: MiRemoteUser, note: IObject, silent = false): Promise { const uri = getApId(note); if (typeof note === 'object') { @@ -643,6 +650,10 @@ export class ApInboxService { if (activity.target === actor.featured) { const activityObject = fromTuple(activity.object); + if (isApObject(activityObject) && !isPost(activityObject)) { + return `unsupported featured object type: ${getApType(activityObject)}`; + } + const note = await this.apNoteService.resolveNote(activityObject, { resolver }); if (note == null) return 'note not found'; await this.notePiningService.removePinned(actor, note.id); @@ -787,7 +798,7 @@ export class ApInboxService { } @bindThis - private async update(actor: MiRemoteUser, activity: IUpdate, resolver?: Resolver): Promise { + private async update(actor: MiRemoteUser, activity: IUpdate, resolver?: Resolver): Promise { if (actor.uri !== activity.actor) { return 'skip: invalid actor'; } @@ -806,9 +817,19 @@ export class ApInboxService { await this.apPersonService.updatePerson(actor.uri, resolver, object); return 'ok: Person updated'; } else if (getApType(object) === 'Question') { + // If we get an Update(Question) for a note that doesn't exist, then create it instead + if (!await this.apNoteService.hasNote(object)) { + return await this.create(actor, activity, resolver); + } + await this.apQuestionService.updateQuestion(object, actor, resolver).catch(err => console.error(err)); return 'ok: Question updated'; } else if (isPost(object)) { + // If we get an Update(Note) for a note that doesn't exist, then create it instead + if (!await this.apNoteService.hasNote(object)) { + return await this.create(actor, activity, resolver); + } + await this.apNoteService.updateNote(object, actor, resolver).catch(err => console.error(err)); return 'ok: Note updated'; } else { diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 51dd357cc5..7d65944488 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -142,6 +142,15 @@ export class ApNoteService { return await this.apDbResolverService.getNoteFromApId(object); } + /** + * Returns true if the provided object / ID exists in the local database. + */ + @bindThis + public async hasNote(object: string | IObject | [string | IObject]): Promise { + const uri = getApId(object); + return await this.notesRepository.existsBy({ uri }); + } + /** * Noteを作成します。 */ diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index c5c0bb8c94..8b1218df47 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -343,6 +343,7 @@ export interface IMove extends IActivity { target: IObject | string; } +export const isApObject = (object: string | IObject): object is IObject => typeof(object) === 'object'; export const isCreate = (object: IObject): object is ICreate => getApType(object) === 'Create'; export const isDelete = (object: IObject): object is IDelete => getApType(object) === 'Delete'; export const isUpdate = (object: IObject): object is IUpdate => getApType(object) === 'Update'; diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 3ad4577706..d6504a798a 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -16,6 +16,7 @@ import { bindThis } from '@/decorators.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; +import { isPackedPureRenote } from '@/misc/is-renote.js'; import type { OnModuleInit } from '@nestjs/common'; import type { CacheService } from '../CacheService.js'; import type { CustomEmojiService } from '../CustomEmojiService.js'; @@ -180,10 +181,9 @@ export class NoteEntityService implements OnModuleInit { } else { // フォロワーかどうか // TODO: 当関数呼び出しごとにクエリが走るのは重そうだからなんとかする - const appearNote = packedNote.renote ?? packedNote; const isFollowing = await this.followingsRepository.exists({ where: { - followeeId: appearNote.userId, + followeeId: packedNote.userId, followerId: meId, }, }); @@ -193,6 +193,14 @@ export class NoteEntityService implements OnModuleInit { } } + // If this is a pure renote (boost), then we should *also* check the boosted note's visibility. + // Otherwise we can have empty notes on the timeline, which is not good. + // Notes are packed in depth-first order, so we can safely grab the "isHidden" property to avoid duplicated checks. + // This is pulled out to ensure that we check both the renote *and* the boosted note. + if (packedNote.renote?.isHidden && isPackedPureRenote(packedNote)) { + hide = true; + } + if (!hide && meId && packedNote.userId !== meId) { const isBlocked = (await this.cacheService.userBlockedCache.fetch(meId)).has(packedNote.userId); diff --git a/packages/backend/src/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts index 43193d12f5..d6872de46a 100644 --- a/packages/backend/src/misc/is-renote.ts +++ b/packages/backend/src/misc/is-renote.ts @@ -71,6 +71,14 @@ type PackedQuote = fileIds: NonNullable['fileIds']> }); +type PackedPureRenote = PackedRenote & { + text: NonNullable['text']>; + cw: NonNullable['cw']>; + replyId: NonNullable['replyId']>; + poll: NonNullable['poll']>; + fileIds: NonNullable['fileIds']>; +} + export function isRenotePacked(note: Packed<'Note'>): note is PackedRenote { return note.renoteId != null; } @@ -82,3 +90,7 @@ export function isQuotePacked(note: PackedRenote): note is PackedQuote { note.poll != null || (note.fileIds != null && note.fileIds.length > 0); } + +export function isPackedPureRenote(note: Packed<'Note'>): note is PackedPureRenote { + return isRenotePacked(note) && !isQuotePacked(note); +} diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 28a74bbb4a..dd183cd991 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -11,6 +11,7 @@ import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; +import { StatusError } from '@/misc/status-error.js'; import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js'; import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js'; import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js'; @@ -134,7 +135,7 @@ export class QueueProcessorService implements OnApplicationShutdown { // 何故かeがundefinedで来ることがある if (!e) return '?'; - if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError') { + if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError' || e instanceof StatusError) { return `${e.name}: ${e.message}`; } @@ -148,12 +149,15 @@ export class QueueProcessorService implements OnApplicationShutdown { function renderJob(job?: Bull.Job) { if (!job) return '?'; - return { - name: job.name || undefined, + const info: Record = { info: getJobInfo(job), - failedReason: job.failedReason || undefined, data: job.data, }; + + if (job.name) info.name = job.name; + if (job.failedReason) info.failedReason = job.failedReason; + + return info; } //#region system diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index f5a356db55..9026331dff 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -7,6 +7,7 @@ import { URL } from 'node:url'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import httpSignature from '@peertube/http-signature'; import * as Bull from 'bullmq'; +import { AbortError } from 'node-fetch'; import type Logger from '@/logger.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js'; @@ -238,6 +239,19 @@ export class InboxProcessorService implements OnApplicationShutdown { return e.message; } } + + if (e instanceof StatusError) { + if (e.isRetryable) { + return `temporary error ${e.statusCode}`; + } else { + return `skip: permanent error ${e.statusCode}`; + } + } + + if (e instanceof AbortError) { + return 'request aborted'; + } + throw e; } return 'ok'; diff --git a/packages/backend/src/server/api/openapi/gen-spec.ts b/packages/backend/src/server/api/openapi/gen-spec.ts index fb5954fee0..82ee0f47d7 100644 --- a/packages/backend/src/server/api/openapi/gen-spec.ts +++ b/packages/backend/src/server/api/openapi/gen-spec.ts @@ -183,7 +183,7 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) { }, ...(endpoint.meta.limit ? { '429': { - description: 'To many requests', + description: 'Too many requests', content: { 'application/json': { schema: { diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 587793e310..0e9d0ff2bb 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -76,6 +76,9 @@ "vue": "3.5.12", "vuedraggable": "next" }, + "optionalDependencies": { + "cypress": "13.15.2" + }, "devDependencies": { "@misskey-dev/summaly": "5.1.0", "@storybook/addon-actions": "8.4.4", @@ -116,7 +119,6 @@ "@vue/runtime-core": "3.5.12", "acorn": "8.14.0", "cross-env": "7.0.3", - "cypress": "13.15.2", "eslint-plugin-import": "2.31.0", "eslint-plugin-vue": "9.31.0", "fast-glob": "3.3.2", diff --git a/packages/frontend/src/components/SkFollowingRecentNotes.vue b/packages/frontend/src/components/SkFollowingRecentNotes.vue new file mode 100644 index 0000000000..6daa8feba5 --- /dev/null +++ b/packages/frontend/src/components/SkFollowingRecentNotes.vue @@ -0,0 +1,144 @@ + + + + + + + diff --git a/packages/frontend/src/components/SkRemoteFollowersWarning.vue b/packages/frontend/src/components/SkRemoteFollowersWarning.vue new file mode 100644 index 0000000000..ceebbd59dd --- /dev/null +++ b/packages/frontend/src/components/SkRemoteFollowersWarning.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/packages/frontend/src/components/SkUserRecentNotes.vue b/packages/frontend/src/components/SkUserRecentNotes.vue index f355facb51..908affcdaf 100644 --- a/packages/frontend/src/components/SkUserRecentNotes.vue +++ b/packages/frontend/src/components/SkUserRecentNotes.vue @@ -101,7 +101,7 @@ onMounted(async () => { margin-bottom: 12px; } -@container (min-width: 451px) { +@container (min-width: 750px) { .userInfo { margin-bottom: 24px; } diff --git a/packages/frontend/src/components/global/SkLazy.vue b/packages/frontend/src/components/global/SkLazy.vue new file mode 100644 index 0000000000..40add97db7 --- /dev/null +++ b/packages/frontend/src/components/global/SkLazy.vue @@ -0,0 +1,57 @@ + + + + + + + + + diff --git a/packages/frontend/src/pages/following-feed.vue b/packages/frontend/src/pages/following-feed.vue index b57759ade0..7fb13c8fcb 100644 --- a/packages/frontend/src/pages/following-feed.vue +++ b/packages/frontend/src/pages/following-feed.vue @@ -7,61 +7,41 @@ SPDX-License-Identifier: AGPL-3.0-only
- {{ i18n.ts.remoteFollowersWarning }} +
- - - - - - - +
-
+ -
+
@@ -257,22 +158,13 @@ definePageMetadata(() => ({ margin-bottom: 12px; } -@keyframes border { - from {border-left: 0px solid var(--accent);} - to {border-left: 6px solid var(--accent);} -} - -.selected { - animation: border 0.2s ease-out 0s 1 forwards; - &:first-child { - border-top-left-radius: 5px; - } - &:last-child { - border-bottom-left-radius: 5px; +@container (max-width: 749px) { + .user { + display: none; } } -@media (min-width: 750px) { +@container (min-width: 750px) { .root { grid-template-columns: min-content 4fr 6fr min-content; grid-template-rows: min-content 1fr; @@ -290,8 +182,4 @@ definePageMetadata(() => ({ margin-bottom: 24px; } } - -.panel { - background: var(--MI_THEME-panel); -} diff --git a/packages/frontend/src/pages/user/recent-notes.vue b/packages/frontend/src/pages/user/recent-notes.vue index c16a1c5fed..d636068408 100644 --- a/packages/frontend/src/pages/user/recent-notes.vue +++ b/packages/frontend/src/pages/user/recent-notes.vue @@ -4,16 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only --> + + diff --git a/packages/frontend/src/scripts/following-feed-utils.ts b/packages/frontend/src/scripts/following-feed-utils.ts index 064d6b72e3..39f17949d6 100644 --- a/packages/frontend/src/scripts/following-feed-utils.ts +++ b/packages/frontend/src/scripts/following-feed-utils.ts @@ -3,19 +3,75 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { computed } from 'vue'; +import { computed, Ref, WritableComputedRef } from 'vue'; import { defaultStore } from '@/store.js'; import { deepMerge } from '@/scripts/merge.js'; import { PageHeaderItem } from '@/types/page-header.js'; import { i18n } from '@/i18n.js'; import { popupMenu } from '@/os.js'; +import { MenuItem } from '@/types/menu.js'; export const followingTab = 'following' as const; export const mutualsTab = 'mutuals' as const; export const followersTab = 'followers' as const; -export type FollowingFeedTab = typeof followingTab | typeof mutualsTab | typeof followersTab; +export const followingFeedTabs = [followingTab, mutualsTab, followersTab] as const; +export type FollowingFeedTab = typeof followingFeedTabs[number]; -export function createOptions(): PageHeaderItem { +export function followingTabName(tab: FollowingFeedTab): string; +export function followingTabName(tab: FollowingFeedTab | null | undefined): null; +export function followingTabName(tab: FollowingFeedTab | null | undefined): string | null { + if (tab === followingTab) return i18n.ts.following; + if (tab === followersTab) return i18n.ts.followers; + if (tab === mutualsTab) return i18n.ts.mutuals; + return null; +} + +export function followingTabIcon(tab: FollowingFeedTab | null | undefined): string { + if (tab === followersTab) return 'ph-user ph-bold ph-lg'; + if (tab === mutualsTab) return 'ph-user-switch ph-bold ph-lg'; + return 'ph-user-check ph-bold ph-lg'; +} + +export type FollowingFeedModel = { + [Key in keyof FollowingFeedState]: WritableComputedRef; +} + +export interface FollowingFeedState { + withNonPublic: boolean, + withQuotes: boolean, + withBots: boolean, + withReplies: boolean, + onlyFiles: boolean, + userList: FollowingFeedTab, + remoteWarningDismissed: boolean, +} + +export const defaultFollowingFeedState: FollowingFeedState = { + withNonPublic: false, + withQuotes: false, + withBots: true, + withReplies: false, + onlyFiles: false, + userList: followingTab, + remoteWarningDismissed: false, +}; + +interface StorageInterface = Partial> { + readonly state: Partial; + readonly reactiveState: Ref>; + save(updated: T): void; +} + +export function createHeaderItem(storage?: Ref): PageHeaderItem { + const menu = createOptionsMenu(storage); + return { + icon: 'ti ti-dots', + text: i18n.ts.options, + handler: ev => popupMenu(menu, ev.currentTarget ?? ev.target), + }; +} + +export function createOptionsMenu(storage?: Ref): MenuItem[] { const { userList, withNonPublic, @@ -23,80 +79,83 @@ export function createOptions(): PageHeaderItem { withBots, withReplies, onlyFiles, - } = createModel(); + } = createModel(storage); - return { - icon: 'ti ti-dots', - text: i18n.ts.options, - handler: ev => - popupMenu([ - { - type: 'switch', - text: i18n.ts.showNonPublicNotes, - ref: withNonPublic, - disabled: userList.value === 'followers', - }, - { - 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), - }; + return [ + { + type: 'switch', + text: i18n.ts.showNonPublicNotes, + ref: withNonPublic, + disabled: computed(() => userList.value === followersTab), + }, + { + 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, + }, + ]; } -export function createModel() { - const userList = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.userList, +export function createModel(storage?: Ref): FollowingFeedModel { + // eslint-disable-next-line no-param-reassign + storage ??= createDefaultStorage(); + + // Based on timeline.saveTlFilter() + const saveFollowingFilter = (key: K, value: FollowingFeedState[K]) => { + const state = deepMerge(storage.value.state, defaultFollowingFeedState); + const out = deepMerge({ [key]: value }, state); + storage.value.save(out); + }; + + const userList: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.userList ?? defaultFollowingFeedState.userList, set: value => saveFollowingFilter('userList', value), }); - - const withNonPublic = computed({ + const withNonPublic: WritableComputedRef = computed({ get: () => { if (userList.value === 'followers') return false; - return defaultStore.reactiveState.followingFeed.value.withNonPublic; + return storage.value.reactiveState.value.withNonPublic ?? defaultFollowingFeedState.withNonPublic; }, set: value => saveFollowingFilter('withNonPublic', value), }); - const withQuotes = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.withQuotes, + const withQuotes: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.withQuotes ?? defaultFollowingFeedState.withQuotes, set: value => saveFollowingFilter('withQuotes', value), }); - const withBots = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.withBots, + const withBots: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.withBots ?? defaultFollowingFeedState.withBots, set: value => saveFollowingFilter('withBots', value), }); - const withReplies = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.withReplies, + const withReplies: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.withReplies ?? defaultFollowingFeedState.withReplies, set: value => saveFollowingFilter('withReplies', value), }); - const onlyFiles = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.onlyFiles, + const onlyFiles: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.onlyFiles ?? defaultFollowingFeedState.onlyFiles, set: value => saveFollowingFilter('onlyFiles', value), }); - - const remoteWarningDismissed = computed({ - get: () => defaultStore.reactiveState.followingFeed.value.remoteWarningDismissed, + const remoteWarningDismissed: WritableComputedRef = computed({ + get: () => storage.value.reactiveState.value.remoteWarningDismissed ?? defaultFollowingFeedState.remoteWarningDismissed, set: value => saveFollowingFilter('remoteWarningDismissed', value), }); @@ -111,8 +170,12 @@ export function createModel() { }; } -// Based on timeline.saveTlFilter() -function saveFollowingFilter(key: Key, value: (typeof defaultStore.state.followingFeed)[Key]) { - const out = deepMerge({ [key]: value }, defaultStore.state.followingFeed); - return defaultStore.set('followingFeed', out); +function createDefaultStorage() { + return computed(() => ({ + state: defaultStore.state.followingFeed, + reactiveState: defaultStore.reactiveState.followingFeed, + save(updated: typeof defaultStore.state.followingFeed) { + return defaultStore.set('followingFeed', updated); + }, + })); } diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts index 9794a300da..89fdda0cbb 100644 --- a/packages/frontend/src/scripts/merge.ts +++ b/packages/frontend/src/scripts/merge.ts @@ -18,7 +18,7 @@ function isPureObject(value: unknown): value is Record>(value: DeepPartial, def: X): X { +export function deepMerge(value: DeepPartial, def: X): X { if (isPureObject(value) && isPureObject(def)) { const result = deepClone(value as Cloneable) as X; for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) { diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 11c76db2e3..c34e0bbf48 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -10,9 +10,9 @@ import lightTheme from '@@/themes/l-cherry.json5'; import darkTheme from '@@/themes/d-ice.json5'; import { searchEngineMap } from './scripts/search-engine-map.js'; import type { SoundType } from '@/scripts/sound.js'; -import type { FollowingFeedTab } from '@/scripts/following-feed-utils.js'; import { DEFAULT_DEVICE_KIND, type DeviceKind } from '@/scripts/device-kind.js'; import { miLocalStorage } from '@/local-storage.js'; +import { defaultFollowingFeedState } from '@/scripts/following-feed-utils.js'; import { Storage } from '@/pizzax.js'; import type { Ast } from '@syuilo/aiscript'; @@ -250,15 +250,7 @@ export const defaultStore = markRaw(new Storage('base', { }, followingFeed: { where: 'account', - default: { - withNonPublic: false, - withQuotes: false, - withBots: true, - withReplies: false, - onlyFiles: false, - userList: 'following' as FollowingFeedTab, - remoteWarningDismissed: false, - }, + default: defaultFollowingFeedState, }, overridedDeviceKind: { diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index 0d82558a00..c0ea833546 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -18,16 +18,21 @@ SPDX-License-Identifier: AGPL-3.0-only :style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }" @wheel.self="onWheel" > - + + + +
{{ i18n.ts._deck.introduction }}
@@ -118,6 +123,7 @@ import XWidgetsColumn from '@/ui/deck/widgets-column.vue'; import XMentionsColumn from '@/ui/deck/mentions-column.vue'; import XDirectColumn from '@/ui/deck/direct-column.vue'; import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue'; +import XFollowingColumn from '@/ui/deck/following-column.vue'; import { mainRouter } from '@/router/main.js'; const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue')); const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue')); @@ -133,6 +139,7 @@ const columnComponents = { mentions: XMentionsColumn, direct: XDirectColumn, roleTimeline: XRoleTimelineColumn, + following: XFollowingColumn, }; mainRouter.navHook = (path, flag): boolean => { diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts index 3186982349..91859b46d7 100644 --- a/packages/frontend/src/ui/deck/deck-store.ts +++ b/packages/frontend/src/ui/deck/deck-store.ts @@ -4,7 +4,7 @@ */ import { throttle } from 'throttle-debounce'; -import { markRaw } from 'vue'; +import { computed, markRaw, Ref } from 'vue'; import { notificationTypes } from 'misskey-js'; import type { BasicTimelineType } from '@/timelines.js'; import { Storage } from '@/pizzax.js'; @@ -29,6 +29,7 @@ export const columnTypes = [ 'mentions', 'direct', 'roleTimeline', + 'following', ] as const; export type ColumnType = typeof columnTypes[number]; @@ -113,8 +114,8 @@ export const loadDeck = async () => { }; // TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する -export const saveDeck = throttle(1000, () => { - misskeyApi('i/registry/set', { +export const saveDeck = throttle(1000, async () => { + await misskeyApi('i/registry/set', { scope: ['client', 'deck', 'profiles'], key: deckStore.state.profile, value: { @@ -314,7 +315,7 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat saveDeck(); } -export function updateColumn(id: Column['id'], column: Partial) { +export async function updateColumn(id: Column['id'], column: Partial) { const columns = deepClone(deckStore.state.columns); const columnIndex = deckStore.state.columns.findIndex(c => c.id === id); const currentColumn = deepClone(deckStore.state.columns[columnIndex]); @@ -323,6 +324,18 @@ export function updateColumn(id: Column['id'], column: Partial) { currentColumn[k] = v; } columns[columnIndex] = currentColumn; - deckStore.set('columns', columns); - saveDeck(); + await Promise.all([ + deckStore.set('columns', columns), + saveDeck(), + ]); +} + +export function getColumn(id: Column['id']): TColumn { + return deckStore.state.columns.find(c => c.id === id) as TColumn; +} + +export function getReactiveColumn(id: Column['id']): Ref { + return computed(() => { + return deckStore.reactiveState.columns.value.find(c => c.id === id) as TColumn; + }); } diff --git a/packages/frontend/src/ui/deck/following-column.vue b/packages/frontend/src/ui/deck/following-column.vue new file mode 100644 index 0000000000..6b8c9db917 --- /dev/null +++ b/packages/frontend/src/ui/deck/following-column.vue @@ -0,0 +1,124 @@ + + + + + + + + + + + diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index fd6132e467..130e10e334 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -11135,7 +11135,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -11654,7 +11654,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -11721,7 +11721,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -12115,7 +12115,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -12175,7 +12175,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -12298,7 +12298,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -13900,7 +13900,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -14735,7 +14735,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -15082,7 +15082,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -15209,7 +15209,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -15704,7 +15704,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16187,7 +16187,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16247,7 +16247,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16310,7 +16310,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16369,7 +16369,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16429,7 +16429,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -16936,7 +16936,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -17211,7 +17211,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -17772,7 +17772,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -17919,7 +17919,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -17986,7 +17986,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18099,7 +18099,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18158,7 +18158,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18403,7 +18403,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18462,7 +18462,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18513,7 +18513,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18564,7 +18564,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18625,7 +18625,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18676,7 +18676,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18727,7 +18727,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18778,7 +18778,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18829,7 +18829,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18880,7 +18880,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -18931,7 +18931,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19168,7 +19168,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19228,7 +19228,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19288,7 +19288,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19347,7 +19347,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19406,7 +19406,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19465,7 +19465,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19533,7 +19533,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19601,7 +19601,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -19929,7 +19929,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -20652,7 +20652,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -20896,7 +20896,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -20956,7 +20956,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -21326,7 +21326,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -21805,7 +21805,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -21973,7 +21973,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -22472,7 +22472,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -22530,7 +22530,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -22588,7 +22588,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -23447,7 +23447,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -23940,7 +23940,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24188,7 +24188,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24362,7 +24362,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24475,7 +24475,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24613,7 +24613,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -24747,7 +24747,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -25079,7 +25079,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -25146,7 +25146,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -25476,7 +25476,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -26026,7 +26026,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -27305,7 +27305,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -28603,7 +28603,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; @@ -28771,7 +28771,7 @@ export type operations = { 'application/json': components['schemas']['Error']; }; }; - /** @description To many requests */ + /** @description Too many requests */ 429: { content: { 'application/json': components['schemas']['Error']; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b923a32b0e..c77005128b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -45,6 +45,10 @@ importers: typescript: specifier: 5.6.3 version: 5.6.3 + optionalDependencies: + cypress: + specifier: 13.15.2 + version: 13.15.2 devDependencies: '@misskey-dev/eslint-plugin': specifier: 2.0.3 @@ -61,9 +65,6 @@ importers: cross-env: specifier: 7.0.3 version: 7.0.3 - cypress: - specifier: 13.15.2 - version: 13.15.2 eslint: specifier: 9.14.0 version: 9.14.0 @@ -879,6 +880,10 @@ importers: vuedraggable: specifier: next version: 4.1.0(vue@3.5.12(typescript@5.6.3)) + optionalDependencies: + cypress: + specifier: 13.15.2 + version: 13.15.2 devDependencies: '@misskey-dev/summaly': specifier: 5.1.0 @@ -997,9 +1002,6 @@ importers: cross-env: specifier: 7.0.3 version: 7.0.3 - cypress: - specifier: 13.15.2 - version: 13.15.2 eslint-plugin-import: specifier: 2.31.0 version: 2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.14.0)(typescript@5.6.3))(eslint@9.14.0) @@ -12273,6 +12275,7 @@ snapshots: tough-cookie: 5.0.0 tunnel-agent: 0.6.0 uuid: 8.3.2 + optional: true '@cypress/xvfb@1.2.4(supports-color@8.1.1)': dependencies: @@ -12280,6 +12283,7 @@ snapshots: lodash.once: 4.1.1 transitivePeerDependencies: - supports-color + optional: true '@digitalbazaar/http-client@3.4.1(web-streams-polyfill@4.0.0)': dependencies: @@ -15124,11 +15128,13 @@ snapshots: dependencies: '@types/sinonjs__fake-timers': 8.1.5 - '@types/sinonjs__fake-timers@8.1.1': {} + '@types/sinonjs__fake-timers@8.1.1': + optional: true '@types/sinonjs__fake-timers@8.1.5': {} - '@types/sizzle@2.3.3': {} + '@types/sizzle@2.3.3': + optional: true '@types/stack-utils@2.0.1': {} @@ -15951,7 +15957,8 @@ snapshots: dependencies: tslib: 2.7.0 - astral-regex@2.0.0: {} + astral-regex@2.0.0: + optional: true astring@1.9.0: {} @@ -15965,7 +15972,8 @@ snapshots: asynckit@0.4.0: {} - at-least-node@1.0.0: {} + at-least-node@1.0.0: + optional: true atomic-sleep@1.0.0: {} @@ -15986,9 +15994,11 @@ snapshots: sinon: 16.1.3 tslib: 2.6.2 - aws-sign2@0.7.0: {} + aws-sign2@0.7.0: + optional: true - aws4@1.12.0: {} + aws4@1.12.0: + optional: true axios@0.24.0: dependencies: @@ -16142,7 +16152,8 @@ snapshots: binary-extensions@2.2.0: {} - blob-util@2.0.2: {} + blob-util@2.0.2: + optional: true bluebird@3.7.2: {} @@ -16219,7 +16230,8 @@ snapshots: dependencies: node-int64: 0.4.0 - buffer-crc32@0.2.13: {} + buffer-crc32@0.2.13: + optional: true buffer-crc32@1.0.0: {} @@ -16236,6 +16248,7 @@ snapshots: dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + optional: true buffer@6.0.3: dependencies: @@ -16323,7 +16336,8 @@ snapshots: normalize-url: 6.1.0 responselike: 2.0.1 - cachedir@2.3.0: {} + cachedir@2.3.0: + optional: true call-bind@1.0.2: dependencies: @@ -16367,7 +16381,8 @@ snapshots: canvas-confetti@1.9.3: {} - caseless@0.12.0: {} + caseless@0.12.0: + optional: true cbor@9.0.2: dependencies: @@ -16508,7 +16523,8 @@ snapshots: ci-info@3.7.1: {} - ci-info@4.1.0: {} + ci-info@4.1.0: + optional: true cjs-module-lexer@1.2.2: {} @@ -16521,6 +16537,7 @@ snapshots: cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 + optional: true cli-highlight@2.1.11: dependencies: @@ -16536,11 +16553,13 @@ snapshots: string-width: 4.2.3 optionalDependencies: '@colors/colors': 1.5.0 + optional: true cli-truncate@2.1.0: dependencies: slice-ansi: 3.0.0 string-width: 4.2.3 + optional: true cli-width@4.1.0: {} @@ -16598,7 +16617,8 @@ snapshots: colord@2.9.3: {} - colorette@2.0.19: {} + colorette@2.0.19: + optional: true combined-stream@1.0.8: dependencies: @@ -16612,7 +16632,8 @@ snapshots: commander@2.20.3: {} - commander@6.2.1: {} + commander@6.2.1: + optional: true commander@7.2.0: {} @@ -16620,7 +16641,8 @@ snapshots: commander@9.5.0: {} - common-tags@1.8.2: {} + common-tags@1.8.2: + optional: true compare-versions@6.1.1: {} @@ -16875,6 +16897,7 @@ snapshots: tree-kill: 1.2.2 untildify: 4.0.0 yauzl: 2.10.0 + optional: true dashdash@1.14.1: dependencies: @@ -17180,6 +17203,7 @@ snapshots: enquirer@2.3.6: dependencies: ansi-colors: 4.1.3 + optional: true entities@2.2.0: {} @@ -17688,7 +17712,8 @@ snapshots: event-target-shim@5.0.1: {} - eventemitter2@6.4.7: {} + eventemitter2@6.4.7: + optional: true eventemitter3@4.0.7: {} @@ -17717,6 +17742,7 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 strip-final-newline: 2.0.0 + optional: true execa@5.1.1: dependencies: @@ -17862,6 +17888,7 @@ snapshots: '@types/yauzl': 2.10.0 transitivePeerDependencies: - supports-color + optional: true extsprintf@1.3.0: {} @@ -17969,6 +17996,7 @@ snapshots: fd-slicer@1.1.0: dependencies: pend: 1.2.0 + optional: true feed@4.2.2: dependencies: @@ -17982,6 +18010,7 @@ snapshots: figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 + optional: true file-entry-cache@6.0.1: dependencies: @@ -18100,7 +18129,8 @@ snapshots: cross-spawn: 7.0.3 signal-exit: 4.1.0 - forever-agent@0.6.1: {} + forever-agent@0.6.1: + optional: true form-data-encoder@2.1.4: {} @@ -18146,6 +18176,7 @@ snapshots: graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.0 + optional: true fs-minipass@2.1.0: dependencies: @@ -18234,6 +18265,7 @@ snapshots: getos@3.2.1: dependencies: async: 3.2.4 + optional: true getpass@0.1.7: dependencies: @@ -18289,6 +18321,7 @@ snapshots: global-dirs@3.0.1: dependencies: ini: 2.0.0 + optional: true globals@11.12.0: {} @@ -18519,6 +18552,7 @@ snapshots: assert-plus: 1.0.0 jsprim: 2.0.2 sshpk: 1.18.0 + optional: true http2-wrapper@1.0.3: dependencies: @@ -18546,7 +18580,8 @@ snapshots: transitivePeerDependencies: - supports-color - human-signals@1.1.1: {} + human-signals@1.1.1: + optional: true human-signals@2.1.0: {} @@ -18610,7 +18645,8 @@ snapshots: ini@1.3.8: {} - ini@2.0.0: {} + ini@2.0.0: + optional: true insert-text-at-cursor@0.3.0: {} @@ -18741,6 +18777,7 @@ snapshots: dependencies: global-dirs: 3.0.1 is-path-inside: 3.0.3 + optional: true is-ip@3.1.0: dependencies: @@ -18821,7 +18858,8 @@ snapshots: dependencies: which-typed-array: 1.1.15 - is-typedarray@1.0.0: {} + is-typedarray@1.0.0: + optional: true is-unicode-supported@0.1.0: {} @@ -18850,7 +18888,8 @@ snapshots: isexe@3.1.1: {} - isstream@0.1.2: {} + isstream@0.1.2: + optional: true istanbul-lib-coverage@3.2.2: {} @@ -19424,6 +19463,7 @@ snapshots: universalify: 2.0.0 optionalDependencies: graceful-fs: 4.2.11 + optional: true jsonld@8.3.2(web-streams-polyfill@4.0.0): dependencies: @@ -19449,6 +19489,7 @@ snapshots: extsprintf: 1.3.0 json-schema: 0.4.0 verror: 1.10.0 + optional: true jsrsasign@11.1.0: {} @@ -19535,6 +19576,7 @@ snapshots: wrap-ansi: 7.0.0 optionalDependencies: enquirer: 2.3.6 + optional: true local-pkg@0.5.0: dependencies: @@ -19559,7 +19601,8 @@ snapshots: lodash.merge@4.6.2: {} - lodash.once@4.1.1: {} + lodash.once@4.1.1: + optional: true lodash.uniq@4.5.0: {} @@ -19576,6 +19619,7 @@ snapshots: cli-cursor: 3.1.0 slice-ansi: 4.0.0 wrap-ansi: 6.2.0 + optional: true longest-streak@3.1.0: {} @@ -20509,7 +20553,8 @@ snapshots: os-utils@0.0.14: {} - ospath@1.2.2: {} + ospath@1.2.2: + optional: true otpauth@9.3.4: dependencies: @@ -20652,9 +20697,11 @@ snapshots: peek-readable@5.3.1: {} - pend@1.2.0: {} + pend@1.2.0: + optional: true - performance-now@2.1.0: {} + performance-now@2.1.0: + optional: true pg-cloudflare@1.1.1: optional: true @@ -20977,7 +21024,8 @@ snapshots: prettier@3.3.3: {} - pretty-bytes@5.6.0: {} + pretty-bytes@5.6.0: + optional: true pretty-format@27.5.1: dependencies: @@ -21047,7 +21095,8 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - proxy-from-env@1.0.0: {} + proxy-from-env@1.0.0: + optional: true proxy-from-env@1.1.0: {} @@ -21369,6 +21418,7 @@ snapshots: request-progress@3.0.0: dependencies: throttleit: 1.0.0 + optional: true require-directory@2.1.1: {} @@ -21418,6 +21468,7 @@ snapshots: dependencies: onetime: 5.1.2 signal-exit: 3.0.7 + optional: true ret@0.5.0: {} @@ -21788,12 +21839,14 @@ snapshots: ansi-styles: 4.3.0 astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + optional: true slice-ansi@4.0.0: dependencies: ansi-styles: 4.3.0 astral-regex: 2.0.0 is-fullwidth-code-point: 3.0.0 + optional: true slick@1.12.2: {} @@ -21893,6 +21946,7 @@ snapshots: jsbn: 0.1.1 safer-buffer: 2.1.2 tweetnacl: 0.14.5 + optional: true ssri@10.0.4: dependencies: @@ -22190,7 +22244,8 @@ snapshots: throttle-debounce@5.0.2: {} - throttleit@1.0.0: {} + throttleit@1.0.0: + optional: true through@2.3.8: {} @@ -22208,11 +22263,13 @@ snapshots: tinyspy@3.0.2: {} - tldts-core@6.1.63: {} + tldts-core@6.1.63: + optional: true tldts@6.1.63: dependencies: tldts-core: 6.1.63 + optional: true tmp@0.2.3: {} @@ -22256,6 +22313,7 @@ snapshots: tough-cookie@5.0.0: dependencies: tldts: 6.1.63 + optional: true tr46@0.0.3: {} @@ -22265,7 +22323,8 @@ snapshots: trace-redirect@1.0.6: {} - tree-kill@1.2.2: {} + tree-kill@1.2.2: + optional: true trim-lines@3.0.1: {} @@ -22357,6 +22416,7 @@ snapshots: tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 + optional: true tweetnacl@0.14.5: {} @@ -22557,7 +22617,8 @@ snapshots: universalify@0.2.0: {} - universalify@2.0.0: {} + universalify@2.0.0: + optional: true unload@2.4.1: {} @@ -22570,7 +22631,8 @@ snapshots: webpack-sources: 3.2.3 webpack-virtual-modules: 0.5.0 - untildify@4.0.0: {} + untildify@4.0.0: + optional: true update-browserslist-db@1.0.13(browserslist@4.22.2): dependencies: @@ -22617,7 +22679,8 @@ snapshots: uuid@10.0.0: {} - uuid@8.3.2: {} + uuid@8.3.2: + optional: true uuid@9.0.1: {} @@ -23155,6 +23218,7 @@ snapshots: dependencies: buffer-crc32: 0.2.13 fd-slicer: 1.1.0 + optional: true yocto-queue@0.1.0: {} diff --git a/sharkey-locales/en-US.yml b/sharkey-locales/en-US.yml index 163fd0b0ae..1f9b6ef4f5 100644 --- a/sharkey-locales/en-US.yml +++ b/sharkey-locales/en-US.yml @@ -397,3 +397,8 @@ _auth: allowed: "Allowed" _announcement: new: "New" +_deck: + _columns: + following: "Following" + +selectFollowRelationship: "Select a follow relationship..."