♻️ Merge branch 'feature/remove-timelines'

This commit is contained in:
ひでまる 2025-01-29 11:30:44 +09:00
commit c6a5fbb882
4 changed files with 631 additions and 598 deletions

View file

@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="pageMetadata.avatar" :class="$style.titleAvatarContainer">
<MkAvatar :class="$style.titleAvatar" :user="pageMetadata.avatar" indicator/>
</div>
<i v-else-if="pageMetadata.icon" :class="[$style.titleIcon, pageMetadata.icon]"></i>
<i v-else-if="pageMetadata.icon" class="icon ph-house ph-lg ph-lg ph-bold"></i>
<div :class="$style.title">
<MkUserName v-if="pageMetadata.userName" :user="pageMetadata.userName" :nowrap="true"/>

View file

@ -318,7 +318,7 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList
iconOnly: true,
}))), ...availableBasicTimelines().map(tl => ({
key: tl,
title: i18n.ts._timelines[tl],
title: tl === 'social' ? i18n.ts._timelines["home"] : i18n.ts._timelines[tl],
icon: basicTimelineIconClass(tl),
iconOnly: true,
})), {
@ -331,17 +331,7 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList
title: i18n.ts.lists,
iconOnly: true,
onClick: chooseList,
}, {
icon: 'ti ti-antenna',
title: i18n.ts.antennas,
iconOnly: true,
onClick: chooseAntenna,
}, {
icon: 'ti ti-device-tv',
title: i18n.ts.channel,
iconOnly: true,
onClick: chooseChannel,
}] as Tab[]);
}, ] as Tab[]);
const headerTabsWhenNotLogin = computed(() => [...availableBasicTimelines().map(tl => ({
key: tl,

View file

@ -3,31 +3,31 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { markRaw, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { hemisphere } from '@@/js/intl-const.js';
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 { 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';
import { markRaw, ref } from "vue";
import * as Misskey from "misskey-js";
import { hemisphere } from "@@/js/intl-const.js";
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 { 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";
interface PostFormAction {
title: string,
title: string;
handler: <T>(form: T, update: (key: unknown, value: unknown) => void) => void;
}
interface UserAction {
title: string,
title: string;
handler: (user: Misskey.entities.UserDetailed) => void;
}
interface NoteAction {
title: string,
title: string;
handler: (note: Misskey.entities.Note) => void;
}
@ -44,11 +44,13 @@ interface PageViewInterruptor {
}
/** サウンド設定 */
export type SoundStore = {
type: Exclude<SoundType, '_driveFile_'>;
export type SoundStore =
| {
type: Exclude<SoundType, "_driveFile_">;
volume: number;
} | {
type: '_driveFile_';
}
| {
type: "_driveFile_";
/** ドライブのファイルID */
fileId: string;
@ -57,7 +59,7 @@ export type SoundStore = {
fileUrl: string;
volume: number;
}
};
export const postFormActions: PostFormAction[] = [];
export const userActions: UserAction[] = [];
@ -68,13 +70,14 @@ export const pageViewInterruptors: PageViewInterruptor[] = [];
// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう)
// あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない
export const defaultStore = markRaw(new Storage('base', {
export const defaultStore = markRaw(
new Storage("base", {
accountSetupWizard: {
where: 'account',
where: "account",
default: 0,
},
timelineTutorials: {
where: 'account',
where: "account",
default: {
home: false,
local: false,
@ -83,146 +86,151 @@ export const defaultStore = markRaw(new Storage('base', {
},
},
abusesTutorial: {
where: 'account',
where: "account",
default: false,
},
keepCw: {
where: 'account',
where: "account",
default: true,
},
showFullAcct: {
where: 'account',
where: "account",
default: false,
},
collapseRenotes: {
where: 'account',
where: "account",
default: false,
},
collapseNotesRepliedTo: {
where: 'account',
where: "account",
default: false,
},
collapseFiles: {
where: 'account',
where: "account",
default: false,
},
uncollapseCW: {
where: 'account',
where: "account",
default: false,
},
expandLongNote: {
where: 'device',
where: "device",
default: false,
},
rememberNoteVisibility: {
where: 'account',
where: "account",
default: false,
},
defaultNoteVisibility: {
where: 'account',
default: 'public' as (typeof Misskey.noteVisibilities)[number],
where: "account",
default: "public" as (typeof Misskey.noteVisibilities)[number],
},
defaultNoteLocalOnly: {
where: 'account',
where: "account",
default: false,
},
uploadFolder: {
where: 'account',
where: "account",
default: null as string | null,
},
pastedFileName: {
where: 'account',
default: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
where: "account",
default: "yyyy-MM-dd HH-mm-ss [{{number}}]",
},
keepOriginalUploading: {
where: 'account',
where: "account",
default: false,
},
memo: {
where: 'account',
where: "account",
default: null,
},
reactions: {
where: 'account',
default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
where: "account",
default: ["👍", "❤", "😆", "🤔", "😮", "🎉", "💢", "😥", "😇", "🍮"],
},
pinnedEmojis: {
where: 'account',
where: "account",
default: [],
},
reactionAcceptance: {
where: 'account',
default: 'nonSensitiveOnly' as 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null,
where: "account",
default: "nonSensitiveOnly" as
| "likeOnly"
| "likeOnlyForRemote"
| "nonSensitiveOnly"
| "nonSensitiveOnlyForLocalLikeOnlyForRemote"
| null,
},
like: {
where: 'account',
where: "account",
default: null as string | null,
},
mutedAds: {
where: 'account',
where: "account",
default: [] as string[],
},
autoloadConversation: {
where: 'account',
where: "account",
default: true,
},
showVisibilitySelectorOnBoost: {
where: 'account',
where: "account",
default: true,
},
visibilityOnBoost: {
where: 'account',
default: 'public' as 'public' | 'home' | 'followers',
where: "account",
default: "public" as "public" | "home" | "followers",
},
trustedDomains: {
where: 'account',
where: "account",
default: [] as string[],
},
warnExternalUrl: {
where: 'account',
where: "account",
default: true,
},
menu: {
where: 'deviceAccount',
where: "deviceAccount",
default: [
'notifications',
'explore',
'followRequests',
'-',
'announcements',
'search',
'-',
'favorites',
'drive',
'achievements',
"notifications",
"explore",
"followRequests",
"-",
"announcements",
"search",
"-",
"favorites",
"drive",
"achievements",
],
},
visibility: {
where: 'deviceAccount',
default: 'public' as (typeof Misskey.noteVisibilities)[number],
where: "deviceAccount",
default: "public" as (typeof Misskey.noteVisibilities)[number],
},
localOnly: {
where: 'deviceAccount',
where: "deviceAccount",
default: false,
},
showPreview: {
where: 'device',
where: "device",
default: false,
},
statusbars: {
where: 'deviceAccount',
where: "deviceAccount",
default: [] as {
name: string;
id: string;
type: string;
size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
size: "verySmall" | "small" | "medium" | "large" | "veryLarge";
black: boolean;
props: Record<string, any>;
}[],
},
widgets: {
where: 'account',
where: "account",
default: [] as {
name: string;
id: string;
@ -231,9 +239,15 @@ export const defaultStore = markRaw(new Storage('base', {
}[],
},
tl: {
where: 'deviceAccount',
where: "deviceAccount",
default: {
src: 'home' as 'home' | 'local' | 'social' | 'global' | 'bubble' | `list:${string}`,
src: "social" as
| "home"
| "local"
| "social"
| "global"
| "bubble"
| `list:${string}`,
userList: null as Misskey.entities.UserList | null,
filter: {
withReplies: true,
@ -245,272 +259,276 @@ export const defaultStore = markRaw(new Storage('base', {
},
},
pinnedUserLists: {
where: 'deviceAccount',
where: "deviceAccount",
default: [] as Misskey.entities.UserList[],
},
followingFeed: {
where: 'account',
where: "account",
default: defaultFollowingFeedState,
},
overridedDeviceKind: {
where: 'device',
where: "device",
default: null as DeviceKind | null,
},
serverDisconnectedBehavior: {
where: 'device',
default: 'disabled' as 'quiet' | 'dialog' | 'disabled',
where: "device",
default: "disabled" as "quiet" | "dialog" | "disabled",
},
nsfw: {
where: 'device',
default: 'respect' as 'respect' | 'force' | 'ignore',
where: "device",
default: "respect" as "respect" | "force" | "ignore",
},
highlightSensitiveMedia: {
where: 'device',
where: "device",
default: false,
},
animation: {
where: 'device',
default: !window.matchMedia('(prefers-reduced-motion)').matches,
where: "device",
default: !window.matchMedia("(prefers-reduced-motion)").matches,
},
animatedMfm: {
where: 'device',
default: !window.matchMedia('(prefers-reduced-motion)').matches,
where: "device",
default: !window.matchMedia("(prefers-reduced-motion)").matches,
},
advancedMfm: {
where: 'device',
where: "device",
default: true,
},
showReactionsCount: {
where: 'device',
where: "device",
default: false,
},
enableQuickAddMfmFunction: {
where: 'device',
where: "device",
default: false,
},
loadRawImages: {
where: 'device',
where: "device",
default: false,
},
warnMissingAltText: {
where: 'device',
where: "device",
default: true,
},
enableFaviconNotificationDot: {
where: 'device',
where: "device",
default: true,
},
imageNewTab: {
where: 'device',
where: "device",
default: false,
},
disableShowingAnimatedImages: {
where: 'device',
default: window.matchMedia('(prefers-reduced-motion)').matches,
where: "device",
default: window.matchMedia("(prefers-reduced-motion)").matches,
},
disableCatSpeak: {
where: 'account',
where: "account",
default: false,
},
emojiStyle: {
where: 'device',
default: 'twemoji', // twemoji / fluentEmoji / native
where: "device",
default: "twemoji", // twemoji / fluentEmoji / native
},
menuStyle: {
where: 'device',
default: 'auto' as 'auto' | 'popup' | 'drawer',
where: "device",
default: "auto" as "auto" | "popup" | "drawer",
},
useBlurEffectForModal: {
where: 'device',
default: DEFAULT_DEVICE_KIND === 'desktop',
where: "device",
default: DEFAULT_DEVICE_KIND === "desktop",
},
useBlurEffect: {
where: 'device',
default: DEFAULT_DEVICE_KIND === 'desktop',
where: "device",
default: DEFAULT_DEVICE_KIND === "desktop",
},
showFixedPostForm: {
where: 'device',
where: "device",
default: false,
},
showFixedPostFormInChannel: {
where: 'device',
where: "device",
default: false,
},
showTickerOnReplies: {
where: 'device',
where: "device",
default: false,
},
searchEngine: {
where: 'account',
where: "account",
default: Object.keys(searchEngineMap)[0],
},
noteDesign: {
where: 'device',
default: 'sharkey' as 'sharkey' | 'misskey',
where: "device",
default: "sharkey" as "sharkey" | "misskey",
},
enableInfiniteScroll: {
where: 'device',
where: "device",
default: true,
},
useReactionPickerForContextMenu: {
where: 'device',
where: "device",
default: false,
},
showGapBetweenNotesInTimeline: {
where: 'device',
where: "device",
default: false,
},
darkMode: {
where: 'device',
where: "device",
default: false,
},
instanceTicker: {
where: 'device',
default: 'remote' as 'none' | 'remote' | 'always',
where: "device",
default: "remote" as "none" | "remote" | "always",
},
emojiPickerScale: {
where: 'device',
where: "device",
default: 1,
},
emojiPickerWidth: {
where: 'device',
where: "device",
default: 1,
},
emojiPickerHeight: {
where: 'device',
where: "device",
default: 2,
},
emojiPickerStyle: {
where: 'device',
default: 'auto' as 'auto' | 'popup' | 'drawer',
where: "device",
default: "auto" as "auto" | "popup" | "drawer",
},
recentlyUsedEmojis: {
where: 'device',
where: "device",
default: [] as string[],
},
recentlyUsedUsers: {
where: 'device',
where: "device",
default: [] as string[],
},
defaultSideView: {
where: 'device',
where: "device",
default: false,
},
menuDisplay: {
where: 'device',
default: 'sideFull' as 'sideFull' | 'sideIcon' | 'top',
where: "device",
default: "sideFull" as "sideFull" | "sideIcon" | "top",
},
reportError: {
where: 'device',
where: "device",
default: false,
},
squareAvatars: {
where: 'device',
where: "device",
default: true,
},
showAvatarDecorations: {
where: 'device',
where: "device",
default: true,
},
postFormWithHashtags: {
where: 'device',
where: "device",
default: false,
},
postFormHashtags: {
where: 'device',
default: '',
where: "device",
default: "",
},
themeInitial: {
where: 'device',
where: "device",
default: true,
},
numberOfPageCache: {
where: 'device',
where: "device",
default: 3,
},
numberOfReplies: {
where: 'device',
where: "device",
default: 5,
},
showNoteActionsOnlyHover: {
where: 'device',
where: "device",
default: false,
},
showClipButtonInNoteFooter: {
where: 'device',
where: "device",
default: false,
},
reactionsDisplaySize: {
where: 'device',
default: 'medium' as 'small' | 'medium' | 'large',
where: "device",
default: "medium" as "small" | "medium" | "large",
},
limitWidthOfReaction: {
where: 'device',
where: "device",
default: true,
},
forceShowAds: {
where: 'device',
where: "device",
default: false,
},
oneko: {
where: 'device',
where: "device",
default: false,
},
clickToOpen: {
where: 'device',
where: "device",
default: true,
},
aiChanMode: {
where: 'device',
where: "device",
default: false,
},
devMode: {
where: 'device',
where: "device",
default: false,
},
mediaListWithOneImageAppearance: {
where: 'device',
default: 'expand' as 'expand' | '16_9' | '1_1' | '2_3',
where: "device",
default: "expand" as "expand" | "16_9" | "1_1" | "2_3",
},
notificationPosition: {
where: 'device',
default: 'rightBottom' as 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom',
where: "device",
default: "rightBottom" as
| "leftTop"
| "leftBottom"
| "rightTop"
| "rightBottom",
},
notificationStackAxis: {
where: 'device',
default: 'horizontal' as 'vertical' | 'horizontal',
where: "device",
default: "horizontal" as "vertical" | "horizontal",
},
notificationClickable: {
where: 'device',
where: "device",
default: false,
},
enableCondensedLine: {
where: 'device',
where: "device",
default: true,
},
additionalUnicodeEmojiIndexes: {
where: 'device',
where: "device",
default: {} as Record<string, Record<string, string[]>>,
},
keepScreenOn: {
where: 'device',
where: "device",
default: false,
},
defaultWithReplies: {
where: 'account',
where: "account",
default: false,
},
disableStreamingTimeline: {
where: 'device',
where: "device",
default: false,
},
useGroupedNotifications: {
where: 'device',
where: "device",
default: true,
},
dataSaver: {
where: 'device',
where: "device",
default: {
media: false,
avatar: false,
@ -519,82 +537,83 @@ export const defaultStore = markRaw(new Storage('base', {
} as Record<string, boolean>,
},
enableSeasonalScreenEffect: {
where: 'device',
where: "device",
default: false,
},
dropAndFusion: {
where: 'device',
where: "device",
default: {
bgmVolume: 0.25,
sfxVolume: 1,
},
},
hemisphere: {
where: 'device',
default: hemisphere as 'N' | 'S',
where: "device",
default: hemisphere as "N" | "S",
},
enableHorizontalSwipe: {
where: 'device',
where: "device",
default: true,
},
useNativeUIForVideoAudioPlayer: {
where: 'device',
where: "device",
default: false,
},
keepOriginalFilename: {
where: 'device',
where: "device",
default: true,
},
alwaysConfirmFollow: {
where: 'device',
where: "device",
default: true,
},
confirmWhenRevealingSensitiveMedia: {
where: 'device',
where: "device",
default: false,
},
contextMenu: {
where: 'device',
default: 'app' as 'app' | 'appWithShift' | 'native',
where: "device",
default: "app" as "app" | "appWithShift" | "native",
},
skipNoteRender: {
where: 'device',
where: "device",
default: true,
},
sound_masterVolume: {
where: 'device',
where: "device",
default: 0.3,
},
sound_notUseSound: {
where: 'device',
where: "device",
default: false,
},
sound_useSoundOnlyWhenActive: {
where: 'device',
where: "device",
default: false,
},
sound_note: {
where: 'device',
default: { type: 'syuilo/n-aec', volume: 0 } as SoundStore,
where: "device",
default: { type: "syuilo/n-aec", volume: 0 } as SoundStore,
},
sound_noteMy: {
where: 'device',
default: { type: 'syuilo/n-cea-4va', volume: 1 } as SoundStore,
where: "device",
default: { type: "syuilo/n-cea-4va", volume: 1 } as SoundStore,
},
sound_notification: {
where: 'device',
default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore,
where: "device",
default: { type: "syuilo/n-ea", volume: 1 } as SoundStore,
},
sound_reaction: {
where: 'device',
default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
where: "device",
default: { type: "syuilo/bubble2", volume: 1 } as SoundStore,
},
}));
}),
);
// TODO: 他のタブと永続化されたstateを同期
const PREFIX = 'miux:' as const;
const PREFIX = "miux:" as const;
export type Plugin = {
id: string;
@ -630,7 +649,9 @@ export class ColdDeviceStorage {
public static watchers: Watcher[] = [];
public static get<T extends keyof typeof ColdDeviceStorage.default>(key: T): typeof ColdDeviceStorage.default[T] {
public static get<T extends keyof typeof ColdDeviceStorage.default>(
key: T,
): (typeof ColdDeviceStorage.default)[T] {
// TODO: indexedDBにする
// ただしその際はnullチェックではなくキー存在チェックにしないとダメ
// (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
@ -643,7 +664,9 @@ export class ColdDeviceStorage {
}
public static getAll(): Partial<typeof this.default> {
return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce<Partial<typeof this.default>>((acc, key) => {
return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce<
Partial<typeof this.default>
>((acc, key) => {
const value = localStorage.getItem(PREFIX + key);
if (value != null) {
acc[key] = JSON.parse(value);
@ -652,7 +675,10 @@ export class ColdDeviceStorage {
}, {});
}
public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void {
public static set<T extends keyof typeof ColdDeviceStorage.default>(
key: T,
value: (typeof ColdDeviceStorage.default)[T],
): void {
// 呼び出し側のバグ等で undefined が来ることがある
// undefined を文字列として miLocalStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視
@ -677,7 +703,7 @@ export class ColdDeviceStorage {
const v = ColdDeviceStorage.get(key);
const r = ref(v);
// TODO: このままではwatcherがリークするので開放する方法を考える
this.watch(key, v => {
this.watch(key, (v) => {
r.value = v;
});
return r;
@ -687,14 +713,16 @@ export class ColdDeviceStorage {
* getter/setterを作ります
* vue場で設定コントロールのmodelとして使う用
*/
public static makeGetterSetter<K extends keyof typeof ColdDeviceStorage.default>(key: K) {
public static makeGetterSetter<
K extends keyof typeof ColdDeviceStorage.default,
>(key: K) {
// TODO: VueのcustomRef使うと良い感じになるかも
const valueRef = ColdDeviceStorage.ref(key);
return {
get: () => {
return valueRef.value;
},
set: (value: typeof ColdDeviceStorage.default[K]) => {
set: (value: (typeof ColdDeviceStorage.default)[K]) => {
const val = value;
ColdDeviceStorage.set(key, val);
},

View file

@ -3,50 +3,63 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { $i } from '@/account.js';
import { instance } from '@/instance.js';
import { $i } from "@/account.js";
import { instance } from "@/instance.js";
export const basicTimelineTypes = [
'home',
'local',
'social',
'bubble',
'global',
"social",
// "home",
"local",
// "bubble",
"global",
] as const;
export type BasicTimelineType = typeof basicTimelineTypes[number];
export type BasicTimelineType = (typeof basicTimelineTypes)[number];
export function isBasicTimeline(timeline: string): timeline is BasicTimelineType {
export function isBasicTimeline(
timeline: string,
): timeline is BasicTimelineType {
return basicTimelineTypes.includes(timeline as BasicTimelineType);
}
export function basicTimelineIconClass(timeline: BasicTimelineType): string {
switch (timeline) {
case 'home':
return 'ti ti-home';
case 'local':
return 'ti ti-planet';
case 'social':
return 'ti ti-universe';
case 'bubble':
return 'ph-drop ph-bold ph-lg';
case 'global':
return 'ti ti-whirl';
case "social":
return "ti ti-home";
case "local":
return "ti ti-planet";
// case "social":
// return "ti ti-universe";
// case "bubble":
// return "ph-drop ph-bold ph-lg";
case "global":
return "ti ti-whirl";
}
}
export function isAvailableBasicTimeline(timeline: BasicTimelineType | undefined | null): boolean {
export function isAvailableBasicTimeline(
timeline: BasicTimelineType | undefined | null,
): boolean {
switch (timeline) {
case 'home':
return $i != null;
case 'local':
return ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
case 'social':
// case "home":
// return $i != null;
case "social":
return $i != null && $i.policies.ltlAvailable;
case 'bubble':
return ($i == null && instance.policies.btlAvailable) || ($i != null && $i.policies.btlAvailable);
case 'global':
return ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
case "local":
return (
($i == null && instance.policies.ltlAvailable) ||
($i != null && $i.policies.ltlAvailable)
);
// case "bubble":
// return (
// ($i == null && instance.policies.btlAvailable) ||
// ($i != null && $i.policies.btlAvailable)
// );
case "global":
return (
($i == null && instance.policies.gtlAvailable) ||
($i != null && $i.policies.gtlAvailable)
);
default:
return false;
}
@ -56,6 +69,8 @@ export function availableBasicTimelines(): BasicTimelineType[] {
return basicTimelineTypes.filter(isAvailableBasicTimeline);
}
export function hasWithReplies(timeline: BasicTimelineType | undefined | null): boolean {
return timeline === 'local' || timeline === 'social';
export function hasWithReplies(
timeline: BasicTimelineType | undefined | null,
): boolean {
return timeline === "local" || timeline === "social";
}