Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

frontend timeline fixes & improvements #13727

Merged
merged 18 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
- Fix: ノート詳細ページにおいてCW付き引用リノートのCWボタンのラベルに「引用」が含まれていない問題を修正
- Fix: ダイアログの入力で字数制限に違反していてもEnterキーが押せてしまう問題を修正
- Fix: ダイレクト投稿の宛先が保存されない問題を修正
- Fix: タイムラインページを開いた時、`TLに他の人への返信を含める`がオンのときに`ファイル付きのみ`をオンにできない問題を修正
- Fix: deck uiでタイムラインを切り替えた際にTLの設定項目が更新されず、`TLに他の人への返信を含める`のトグルが表示されない問題を修正
- Fix: ウィジェットのタイムライン選択欄に無効化されたタイムラインが表示される問題を修正

### Server
- Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
Expand Down
3 changes: 2 additions & 1 deletion packages/frontend/src/components/MkTimeline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, watch, onUnmounted, provide, ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import type { TimelineType } from '@/timelines.js';
import MkNotes from '@/components/MkNotes.vue';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
import { useStream } from '@/stream.js';
Expand All @@ -29,7 +30,7 @@ import { defaultStore } from '@/store.js';
import { Paging } from '@/components/MkPagination.vue';

const props = withDefaults(defineProps<{
src: 'home' | 'local' | 'social' | 'global' | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
src: TimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
list?: string;
antenna?: string;
channel?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps">
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._timeline.description1 }}</div>
<div class="_gaps_s">
<div><i class="ti ti-home"></i> <b>{{ i18n.ts._timelines.home }}</b> … {{ i18n.ts._initialTutorial._timeline.home }}</div>
<div><i class="ti ti-planet"></i> <b>{{ i18n.ts._timelines.local }}</b> … {{ i18n.ts._initialTutorial._timeline.local }}</div>
<div><i class="ti ti-universe"></i> <b>{{ i18n.ts._timelines.social }}</b> … {{ i18n.ts._initialTutorial._timeline.social }}</div>
<div><i class="ti ti-whirl"></i> <b>{{ i18n.ts._timelines.global }}</b> … {{ i18n.ts._initialTutorial._timeline.global }}</div>
<div v-for="tl in timelineTypes">
<i :class="timelineIconClass(tl)"></i> <b>{{ i18n.ts._timelines[tl] }}</b> … {{ i18n.ts._initialTutorial._timeline[tl] }}
</div>
</div>
<div class="_gaps_s">
<div>{{ i18n.ts._initialTutorial._timeline.description2 }}</div>
Expand All @@ -22,12 +21,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<a href="https://misskey-hub.net/docs/for-users/features/timeline/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
</template>
</I18n>

</div>
</template>

<script setup lang="ts">
import { i18n } from '@/i18n.js';
import { timelineIconClass, timelineTypes } from '@/timelines.js';
</script>

<style lang="scss" module>
Expand Down
72 changes: 23 additions & 49 deletions packages/frontend/src/pages/timeline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :contentMax="800">
<MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
<div :key="src" ref="rootEl" v-hotkey.global="keymap">
<MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()">
<MkInfo v-if="isTimeline(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()">
{{ i18n.ts._timelineDescription[src] }}
</MkInfo>
<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
Expand Down Expand Up @@ -45,19 +45,17 @@ import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { $i } from '@/account.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { antennasCache, userListsCache } from '@/cache.js';
import { deviceKind } from '@/scripts/device-kind.js';
import { deepMerge } from '@/scripts/merge.js';
import { MenuItem } from '@/types/menu.js';
import { miLocalStorage } from '@/local-storage.js';
import { availableTimelines, hasWithReplies, isAvailableTimeline, isTimeline, timelineIconClass } from '@/timelines.js';

provide('shouldOmitHeaderTitle', true);

const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
const keymap = {
't': focus,
};
Expand All @@ -66,7 +64,7 @@ const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>();
const rootEl = shallowRef<HTMLElement>();

const queue = ref(0);
const srcWhenNotSignin = ref<'local' | 'global'>(isLocalTimelineAvailable ? 'local' : 'global');
const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableTimeline('local') ? 'local' : 'global');
const src = computed<'home' | 'local' | 'social' | 'global' | `list:${string}`>({
get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value),
set: (x) => saveSrc(x),
Expand All @@ -77,7 +75,11 @@ const withRenotes = computed<boolean>({
});

// computed内での無限ループを防ぐためのフラグ
const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>('withReplies');
const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>(
defaultStore.reactiveState.tl.value.filter.withReplies ? 'withReplies' :
defaultStore.reactiveState.tl.value.filter.onlyFiles ? 'onlyFiles' :
false,
);

const withReplies = computed<boolean>({
get: () => {
Expand Down Expand Up @@ -234,7 +236,7 @@ function focus(): void {
}

function closeTutorial(): void {
if (!['home', 'local', 'social', 'global'].includes(src.value)) return;
if (!isTimeline(src.value)) return;
const before = defaultStore.state.timelineTutorials;
before[src.value] = true;
defaultStore.set('timelineTutorials', before);
Expand All @@ -250,7 +252,7 @@ const headerActions = computed(() => {
type: 'switch',
text: i18n.ts.showRenotes,
ref: withRenotes,
}, src.value === 'local' || src.value === 'social' ? {
}, isTimeline(src.value) && hasWithReplies(src.value) ? {
type: 'switch',
text: i18n.ts.showRepliesToOthersInTimeline,
ref: withReplies,
Expand All @@ -263,7 +265,7 @@ const headerActions = computed(() => {
type: 'switch',
text: i18n.ts.fileAttachedOnly,
ref: onlyFiles,
disabled: src.value === 'local' || src.value === 'social' ? withReplies : false,
disabled: isTimeline(src.value) && hasWithReplies(src.value) ? withReplies : false,
}], ev.currentTarget ?? ev.target);
},
},
Expand All @@ -285,32 +287,12 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList
title: l.name,
icon: 'ti ti-star',
iconOnly: true,
}))), {
key: 'home',
title: i18n.ts._timelines.home,
icon: 'ti ti-home',
}))), ...availableTimelines().map(tl => ({
key: tl,
title: i18n.ts._timelines[tl],
icon: timelineIconClass(tl),
iconOnly: true,
}, ...(isLocalTimelineAvailable ? [{
key: 'local',
title: i18n.ts._timelines.local,
icon: 'ti ti-planet',
iconOnly: true,
}, {
key: 'social',
title: i18n.ts._timelines.social,
icon: 'ti ti-universe',
iconOnly: true,
}] : []), ...(isGlobalTimelineAvailable ? [{
key: 'global',
title: i18n.ts._timelines.global,
icon: 'ti ti-whirl',
iconOnly: true,
}] : []), {
icon: 'ti ti-list',
title: i18n.ts.lists,
iconOnly: true,
onClick: chooseList,
}, {
})), {
icon: 'ti ti-antenna',
title: i18n.ts.antennas,
iconOnly: true,
Expand All @@ -322,24 +304,16 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList
onClick: chooseChannel,
}] as Tab[]);

const headerTabsWhenNotLogin = computed(() => [
...(isLocalTimelineAvailable ? [{
key: 'local',
title: i18n.ts._timelines.local,
icon: 'ti ti-planet',
iconOnly: true,
}] : []),
...(isGlobalTimelineAvailable ? [{
key: 'global',
title: i18n.ts._timelines.global,
icon: 'ti ti-whirl',
iconOnly: true,
}] : []),
] as Tab[]);
const headerTabsWhenNotLogin = computed(() => [...availableTimelines().map(tl => ({
key: tl,
title: i18n.ts._timelines[tl],
icon: timelineIconClass(tl),
iconOnly: true,
}))] as Tab[]);

definePageMetadata(() => ({
title: i18n.ts.timeline,
icon: src.value === 'local' ? 'ti ti-planet' : src.value === 'social' ? 'ti ti-universe' : src.value === 'global' ? 'ti ti-whirl' : 'ti ti-home',
icon: isTimeline(src.value) ? timelineIconClass(src.value) : 'ti ti-home',
}));
</script>

Expand Down
56 changes: 56 additions & 0 deletions packages/frontend/src/timelines.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { $i } from '@/account.js';
import { instance } from '@/instance.js';

export const timelineTypes = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

list、antenna、mentionなどは含めなくて良いかしら(含めない場合、timelineという名前は合ってないかも?)

Copy link
Member Author

@anatawa12 anatawa12 Apr 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この配列ではチュートリアルで表示されるようなものを示したかったのでbasic timelineとかでいいですかね

リスト アンテナ に関しては場所によって扱いが違う(標準状態の/timelineでもないしdeckでも別カラム)ので、一旦除外して共通化させました

'home',
'local',
'social',
'global',
] as const;

export type TimelineType = typeof timelineTypes[number];

export function isTimeline(timeline: string): timeline is TimelineType {
return timelineTypes.includes(timeline as TimelineType);
}

export function timelineIconClass(timeline: TimelineType): string {
switch (timeline) {
case 'home':
return 'ti ti-home';
case 'local':
return 'ti ti-planet';
case 'social':
return 'ti ti-universe';
case 'global':
return 'ti ti-whirl';
}
}

export function isAvailableTimeline(timeline: TimelineType | 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':
return $i != null && instance.policies.ltlAvailable;
case 'global':
return ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
default:
return false;
}
}

export function availableTimelines(): TimelineType[] {
return timelineTypes.filter(isAvailableTimeline);
}

export function hasWithReplies(timeline: TimelineType | undefined | null): boolean {
return timeline === 'local' || timeline === 'social';
}
32 changes: 11 additions & 21 deletions packages/frontend/src/ui/deck/tl-column.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
-->

<template>
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => await timeline?.reloadTimeline()">
<template #header>
<i v-if="column.tl === 'home'" class="ti ti-home"></i>
<i v-else-if="column.tl === 'local'" class="ti ti-planet"></i>
<i v-else-if="column.tl === 'social'" class="ti ti-universe"></i>
<i v-else-if="column.tl === 'global'" class="ti ti-whirl"></i>
<i v-if="column.tl != null" :class="timelineIconClass(column.tl)"/>
<span style="margin-left: 8px;">{{ column.name }}</span>
</template>

<div v-if="(((column.tl === 'local' || column.tl === 'social') && !isLocalTimelineAvailable) || (column.tl === 'global' && !isGlobalTimelineAvailable))" :class="$style.disabled">
<div v-if="!isAvailableTimeline(column.tl)" :class="$style.disabled">
<p :class="$style.disabledTitle">
<i class="ti ti-circle-minus"></i>
{{ i18n.ts._disabledTimeline.title }}
Expand All @@ -33,25 +30,22 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>

<script lang="ts" setup>
import { onMounted, watch, ref, shallowRef } from 'vue';
import { onMounted, watch, ref, shallowRef, computed } from 'vue';
import XColumn from './column.vue';
import { removeColumn, updateColumn, Column } from './deck-store.js';
import type { MenuItem } from '@/types/menu.js';
import MkTimeline from '@/components/MkTimeline.vue';
import * as os from '@/os.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { hasWithReplies, isAvailableTimeline, timelineIconClass } from '@/timelines.js';

const props = defineProps<{
column: Column;
isStacked: boolean;
}>();

const disabled = ref(false);
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();

const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));
const withRenotes = ref(props.column.withRenotes ?? true);
const withReplies = ref(props.column.withReplies ?? false);
const onlyFiles = ref(props.column.onlyFiles ?? false);
Expand All @@ -77,10 +71,6 @@ watch(onlyFiles, v => {
onMounted(() => {
if (props.column.tl == null) {
setType();
} else if ($i) {
disabled.value = (
(!((instance.policies.ltlAvailable) || ($i.policies.ltlAvailable)) && ['local', 'social'].includes(props.column.tl)) ||
(!((instance.policies.gtlAvailable) || ($i.policies.gtlAvailable)) && ['global'].includes(props.column.tl)));
}
});

Expand All @@ -104,19 +94,19 @@ async function setType() {
return;
}
updateColumn(props.column.id, {
tl: src,
tl: src ?? undefined,
});
}

const menu = [{
const menu = computed<MenuItem[]>(() => [{
icon: 'ti ti-pencil',
text: i18n.ts.timeline,
action: setType,
}, {
type: 'switch',
text: i18n.ts.showRenotes,
ref: withRenotes,
}, props.column.tl === 'local' || props.column.tl === 'social' ? {
}, hasWithReplies(props.column.tl) ? {
type: 'switch',
text: i18n.ts.showRepliesToOthersInTimeline,
ref: withReplies,
Expand All @@ -125,8 +115,8 @@ const menu = [{
type: 'switch',
text: i18n.ts.fileAttachedOnly,
ref: onlyFiles,
disabled: props.column.tl === 'local' || props.column.tl === 'social' ? withReplies : false,
}];
disabled: hasWithReplies(props.column.tl) ? withReplies : false,
}]);
</script>

<style lang="scss" module>
Expand Down
Loading
Loading