restore following feed deck UI

This commit is contained in:
Hazelnoot 2025-03-27 10:30:04 -04:00
parent 86bfafe27f
commit b4e3062083
4 changed files with 40 additions and 35 deletions

View file

@ -4,7 +4,8 @@
*/ */
import { notificationTypes } from 'misskey-js'; import { notificationTypes } from 'misskey-js';
import { ref } from 'vue'; import { ref, computed } from 'vue';
import type { Ref } from 'vue';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import { i18n } from './i18n.js'; import { i18n } from './i18n.js';
import type { BasicTimelineType } from '@/timelines.js'; import type { BasicTimelineType } from '@/timelines.js';
@ -320,6 +321,12 @@ export function updateColumn(id: Column['id'], column: Partial<Column>) {
saveCurrentDeckProfile(); saveCurrentDeckProfile();
} }
export function getColumn<TColumn extends Column>(id: Column['id']): Ref<TColumn> {
return computed(() => {
return columns.value.find(c => c.id === id) as TColumn;
});
}
export function switchProfileMenu(ev: MouseEvent) { export function switchProfileMenu(ev: MouseEvent) {
const items: MenuItem[] = prefer.s['deck.profile'] ? [{ const items: MenuItem[] = prefer.s['deck.profile'] ? [{
text: prefer.s['deck.profile'], text: prefer.s['deck.profile'],

View file

@ -18,21 +18,22 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts"> <script lang="ts">
import { computed, shallowRef } from 'vue'; import { computed, shallowRef } from 'vue';
import type { Column } from '@/ui/deck/deck-store.js'; import type { Column } from '@/deck.js';
import type { FollowingFeedState } from '@/utility/following-feed-utils.js'; import type { FollowingFeedState } from '@/utility/following-feed-utils.js';
export type FollowingColumn = Column & Partial<FollowingFeedState>; export type FollowingColumn = Column & Partial<FollowingFeedState>;
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import { getColumn, getReactiveColumn, updateColumn } from '@/ui/deck/deck-store.js'; import type { FollowingFeedTab } from '@/utility/following-feed-utils.js';
import type { MenuItem } from '@/types/menu.js';
import { getColumn, updateColumn } from '@/deck.js';
import XColumn from '@/ui/deck/column.vue'; import XColumn from '@/ui/deck/column.vue';
import SkFollowingRecentNotes from '@/components/SkFollowingRecentNotes.vue'; import SkFollowingRecentNotes from '@/components/SkFollowingRecentNotes.vue';
import SkRemoteFollowersWarning from '@/components/SkRemoteFollowersWarning.vue'; import SkRemoteFollowersWarning from '@/components/SkRemoteFollowersWarning.vue';
import { createModel, createOptionsMenu, FollowingFeedTab, followingTab, followingTabName, followingTabIcon, followingFeedTabs } from '@/utility/following-feed-utils.js'; import { createModel, createOptionsMenu, followingTab, followingTabName, followingTabIcon, followingFeedTabs } from '@/utility/following-feed-utils.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { MenuItem } from '@/types/menu.js'; import { useRouter } from '@/router.js';
import { useRouter } from '@/router/supplier.js';
const props = defineProps<{ const props = defineProps<{
column: FollowingColumn; column: FollowingColumn;
@ -53,10 +54,10 @@ async function selectList(): Promise<void> {
if (canceled) return; if (canceled) return;
await updateColumn(props.column.id, { updateColumn(props.column.id, {
name: getNewColumnName(newList), name: getNewColumnName(newList),
userList: newList, userList: newList,
}); } as Partial<FollowingColumn>);
} }
function getNewColumnName(newList: FollowingFeedTab) { function getNewColumnName(newList: FollowingFeedTab) {
@ -78,7 +79,6 @@ if (!props.column.userList) {
// This allows multiple columns to exist with different settings. // This allows multiple columns to exist with different settings.
const columnStorage = computed(() => ({ const columnStorage = computed(() => ({
state: getColumn<FollowingColumn>(props.column.id), state: getColumn<FollowingColumn>(props.column.id),
reactiveState: getReactiveColumn<FollowingColumn>(props.column.id),
save(updated: FollowingColumn) { save(updated: FollowingColumn) {
updateColumn(props.column.id, updated); updateColumn(props.column.id, updated);
}, },

View file

@ -3,13 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { computed, Ref, WritableComputedRef } from 'vue'; import { computed } from 'vue';
import { defaultStore } from '@/store.js'; import type { Ref, WritableComputedRef } from 'vue';
import type { PageHeaderItem } from '@/types/page-header.js';
import type { MenuItem } from '@/types/menu.js';
import { deepMerge } from '@/utility/merge.js'; import { deepMerge } from '@/utility/merge.js';
import { PageHeaderItem } from '@/types/page-header.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { popupMenu } from '@/os.js'; import { popupMenu } from '@/os.js';
import { MenuItem } from '@/types/menu.js'; import { prefer } from '@/preferences';
export const followingTab = 'following' as const; export const followingTab = 'following' as const;
export const mutualsTab = 'mutuals' as const; export const mutualsTab = 'mutuals' as const;
@ -34,7 +35,7 @@ export function followingTabIcon(tab: FollowingFeedTab | null | undefined): stri
export type FollowingFeedModel = { export type FollowingFeedModel = {
[Key in keyof FollowingFeedState]: WritableComputedRef<FollowingFeedState[Key]>; [Key in keyof FollowingFeedState]: WritableComputedRef<FollowingFeedState[Key]>;
} };
export interface FollowingFeedState { export interface FollowingFeedState {
withNonPublic: boolean, withNonPublic: boolean,
@ -56,10 +57,9 @@ export const defaultFollowingFeedState: FollowingFeedState = {
remoteWarningDismissed: false, remoteWarningDismissed: false,
}; };
interface StorageInterface<T extends Partial<FollowingFeedState> = Partial<FollowingFeedState>> { interface StorageInterface {
readonly state: Partial<T>; readonly state: Ref<Partial<FollowingFeedState>>;
readonly reactiveState: Ref<Partial<T>>; save(updated: Partial<FollowingFeedState>): void;
save(updated: T): void;
} }
export function createHeaderItem(storage?: Ref<StorageInterface>): PageHeaderItem { export function createHeaderItem(storage?: Ref<StorageInterface>): PageHeaderItem {
@ -117,45 +117,44 @@ export function createOptionsMenu(storage?: Ref<StorageInterface>): MenuItem[] {
} }
export function createModel(storage?: Ref<StorageInterface>): FollowingFeedModel { export function createModel(storage?: Ref<StorageInterface>): FollowingFeedModel {
// eslint-disable-next-line no-param-reassign
storage ??= createDefaultStorage(); storage ??= createDefaultStorage();
// Based on timeline.saveTlFilter() // Based on timeline.saveTlFilter()
const saveFollowingFilter = <K extends keyof FollowingFeedState>(key: K, value: FollowingFeedState[K]) => { const saveFollowingFilter = <K extends keyof FollowingFeedState>(key: K, value: FollowingFeedState[K]) => {
const state = deepMerge(storage.value.state, defaultFollowingFeedState); const state = deepMerge<FollowingFeedState>(storage.value.state.value, defaultFollowingFeedState);
const out = deepMerge({ [key]: value }, state); const out = deepMerge<FollowingFeedState>({ [key]: value }, state);
storage.value.save(out); storage.value.save(out);
}; };
const userList: WritableComputedRef<FollowingFeedTab> = computed({ const userList: WritableComputedRef<FollowingFeedTab> = computed({
get: () => storage.value.reactiveState.value.userList ?? defaultFollowingFeedState.userList, get: () => storage.value.state.value.userList ?? defaultFollowingFeedState.userList,
set: value => saveFollowingFilter('userList', value), set: value => saveFollowingFilter('userList', value),
}); });
const withNonPublic: WritableComputedRef<boolean> = computed({ const withNonPublic: WritableComputedRef<boolean> = computed({
get: () => { get: () => {
if (userList.value === 'followers') return false; if (userList.value === 'followers') return false;
return storage.value.reactiveState.value.withNonPublic ?? defaultFollowingFeedState.withNonPublic; return storage.value.state.value.withNonPublic ?? defaultFollowingFeedState.withNonPublic;
}, },
set: value => saveFollowingFilter('withNonPublic', value), set: value => saveFollowingFilter('withNonPublic', value),
}); });
const withQuotes: WritableComputedRef<boolean> = computed({ const withQuotes: WritableComputedRef<boolean> = computed({
get: () => storage.value.reactiveState.value.withQuotes ?? defaultFollowingFeedState.withQuotes, get: () => storage.value.state.value.withQuotes ?? defaultFollowingFeedState.withQuotes,
set: value => saveFollowingFilter('withQuotes', value), set: value => saveFollowingFilter('withQuotes', value),
}); });
const withBots: WritableComputedRef<boolean> = computed({ const withBots: WritableComputedRef<boolean> = computed({
get: () => storage.value.reactiveState.value.withBots ?? defaultFollowingFeedState.withBots, get: () => storage.value.state.value.withBots ?? defaultFollowingFeedState.withBots,
set: value => saveFollowingFilter('withBots', value), set: value => saveFollowingFilter('withBots', value),
}); });
const withReplies: WritableComputedRef<boolean> = computed({ const withReplies: WritableComputedRef<boolean> = computed({
get: () => storage.value.reactiveState.value.withReplies ?? defaultFollowingFeedState.withReplies, get: () => storage.value.state.value.withReplies ?? defaultFollowingFeedState.withReplies,
set: value => saveFollowingFilter('withReplies', value), set: value => saveFollowingFilter('withReplies', value),
}); });
const onlyFiles: WritableComputedRef<boolean> = computed({ const onlyFiles: WritableComputedRef<boolean> = computed({
get: () => storage.value.reactiveState.value.onlyFiles ?? defaultFollowingFeedState.onlyFiles, get: () => storage.value.state.value.onlyFiles ?? defaultFollowingFeedState.onlyFiles,
set: value => saveFollowingFilter('onlyFiles', value), set: value => saveFollowingFilter('onlyFiles', value),
}); });
const remoteWarningDismissed: WritableComputedRef<boolean> = computed({ const remoteWarningDismissed: WritableComputedRef<boolean> = computed({
get: () => storage.value.reactiveState.value.remoteWarningDismissed ?? defaultFollowingFeedState.remoteWarningDismissed, get: () => storage.value.state.value.remoteWarningDismissed ?? defaultFollowingFeedState.remoteWarningDismissed,
set: value => saveFollowingFilter('remoteWarningDismissed', value), set: value => saveFollowingFilter('remoteWarningDismissed', value),
}); });
@ -172,10 +171,9 @@ export function createModel(storage?: Ref<StorageInterface>): FollowingFeedModel
function createDefaultStorage() { function createDefaultStorage() {
return computed(() => ({ return computed(() => ({
state: defaultStore.state.followingFeed, state: prefer.s.followingFeed,
reactiveState: defaultStore.reactiveState.followingFeed, save(updated: typeof prefer.s.followingFeed) {
save(updated: typeof defaultStore.state.followingFeed) { prefer.s.followingFeed = updated;
return defaultStore.set('followingFeed', updated);
}, },
})); }));
} }

View file

@ -18,7 +18,7 @@ function isPureObject(value: unknown): value is Record<PropertyKey, unknown> {
* valueにないキーをdefからもらう\ * valueにないキーをdefからもらう\
* nullはそのままundefinedはdefの値 * nullはそのままundefinedはdefの値
**/ **/
export function deepMerge<X extends Record<PropertyKey, unknown>>(value: DeepPartial<X>, def: X): X { export function deepMerge<X extends object | Record<PropertyKey, unknown>>(value: DeepPartial<X>, def: X): X {
if (isPureObject(value) && isPureObject(def)) { if (isPureObject(value) && isPureObject(def)) {
const result = deepClone(value as Cloneable) as X; const result = deepClone(value as Cloneable) as X;
for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) { for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {