merge: fix inconsistent following feed filters on mobile (resolves #776) (!717)

View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/717

Closes #776

Approved-by: Amber Null <puppygirlhornyposting@gmail.com>
Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
Hazelnoot 2024-11-01 15:40:43 +00:00
commit 4da262d98c
4 changed files with 146 additions and 128 deletions

View file

@ -39,13 +39,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</template> </template>
<script lang="ts">
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;
</script>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, Ref, ref, shallowRef } from 'vue'; import { computed, Ref, ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
@ -60,54 +53,24 @@ import { Tab } from '@/components/global/MkPageHeader.tabs.vue';
import { PageHeaderItem } from '@/types/page-header.js'; import { PageHeaderItem } from '@/types/page-header.js';
import SkFollowingFeedEntry from '@/components/SkFollowingFeedEntry.vue'; import SkFollowingFeedEntry from '@/components/SkFollowingFeedEntry.vue';
import { useRouter } from '@/router/supplier.js'; import { useRouter } from '@/router/supplier.js';
import * as os from '@/os.js';
import MkPageHeader from '@/components/global/MkPageHeader.vue'; import MkPageHeader from '@/components/global/MkPageHeader.vue';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { checkWordMute } from '@/scripts/check-word-mute.js'; import { checkWordMute } from '@/scripts/check-word-mute.js';
import SkUserRecentNotes from '@/components/SkUserRecentNotes.vue'; import SkUserRecentNotes from '@/components/SkUserRecentNotes.vue';
import { useScrollPositionManager } from '@/nirax.js'; import { useScrollPositionManager } from '@/nirax.js';
import { defaultStore } from '@/store.js';
import { deepMerge } from '@/scripts/merge.js';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination, { Paging } from '@/components/MkPagination.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import { createModel, createOptions, followersTab, followingTab, mutualsTab } from '@/scripts/following-feed-utils.js';
const withNonPublic = computed({ const {
get: () => { userList,
if (userList.value === 'followers') return false; withNonPublic,
return defaultStore.reactiveState.followingFeed.value.withNonPublic; withQuotes,
}, withBots,
set: value => saveFollowingFilter('withNonPublic', value), withReplies,
}); onlyFiles,
const withQuotes = computed({ remoteWarningDismissed,
get: () => defaultStore.reactiveState.followingFeed.value.withQuotes, } = createModel();
set: value => saveFollowingFilter('withQuotes', value),
});
const withBots = computed({
get: () => defaultStore.reactiveState.followingFeed.value.withBots,
set: value => saveFollowingFilter('withBots', value),
});
const withReplies = computed({
get: () => defaultStore.reactiveState.followingFeed.value.withReplies,
set: value => saveFollowingFilter('withReplies', value),
});
const onlyFiles = computed({
get: () => defaultStore.reactiveState.followingFeed.value.onlyFiles,
set: value => saveFollowingFilter('onlyFiles', value),
});
const userList = computed({
get: () => defaultStore.reactiveState.followingFeed.value.userList,
set: value => saveFollowingFilter('userList', value),
});
const remoteWarningDismissed = computed({
get: () => defaultStore.reactiveState.followingFeed.value.remoteWarningDismissed,
set: value => saveFollowingFilter('remoteWarningDismissed', value),
});
// Based on timeline.saveTlFilter()
function saveFollowingFilter<Key extends keyof typeof defaultStore.state.followingFeed>(key: Key, value: (typeof defaultStore.state.followingFeed)[Key]) {
const out = deepMerge({ [key]: value }, defaultStore.state.followingFeed);
defaultStore.set('followingFeed', out);
}
const router = useRouter(); const router = useRouter();
@ -215,45 +178,7 @@ const headerActions: PageHeaderItem[] = [
text: i18n.ts.reload, text: i18n.ts.reload,
handler: () => reload(), handler: () => reload(),
}, },
{ createOptions(),
icon: 'ti ti-dots',
text: i18n.ts.options,
handler: (ev) => {
os.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);
},
},
]; ];
const headerTabs = computed(() => [ const headerTabs = computed(() => [

View file

@ -4,24 +4,25 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<MkStickyContainer> <MkStickyContainer ref="userScroll">
<template #header> <template #header>
<MkPageHeader :actions="headerActions" :displayBackButton="true"/> <MkPageHeader :actions="headerActions" :displayBackButton="true"/>
</template> </template>
<SkUserRecentNotes ref="userRecentNotes" :userId="userId" :withRenotes="withRenotes" :withReplies="withReplies" :onlyFiles="onlyFiles"/> <SkUserRecentNotes ref="userRecentNotes" :userId="userId" :withNonPublic="withNonPublic" :withQuotes="withQuotes" :withBots="withBots" :withReplies="withReplies" :onlyFiles="onlyFiles"/>
</MkStickyContainer> </MkStickyContainer>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, shallowRef } from 'vue'; import { computed, shallowRef } from 'vue';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { PageHeaderItem } from '@/types/page-header.js'; import { PageHeaderItem } from '@/types/page-header.js';
import * as os from '@/os.js';
import MkPageHeader from '@/components/global/MkPageHeader.vue'; import MkPageHeader from '@/components/global/MkPageHeader.vue';
import SkUserRecentNotes from '@/components/SkUserRecentNotes.vue'; import SkUserRecentNotes from '@/components/SkUserRecentNotes.vue';
import { acct } from '@/filters/user.js'; import { acct } from '@/filters/user.js';
import { createModel, createOptions } from '@/scripts/following-feed-utils.js';
import MkStickyContainer from '@/components/global/MkStickyContainer.vue';
defineProps<{ defineProps<{
userId: string; userId: string;
@ -29,43 +30,22 @@ defineProps<{
const userRecentNotes = shallowRef<InstanceType<typeof SkUserRecentNotes>>(); const userRecentNotes = shallowRef<InstanceType<typeof SkUserRecentNotes>>();
const user = computed(() => userRecentNotes.value?.user); const user = computed(() => userRecentNotes.value?.user);
const withRenotes = ref(false);
const withReplies = ref(true);
const onlyFiles = ref(false);
const headerActions = [ const {
withNonPublic,
withQuotes,
withBots,
withReplies,
onlyFiles,
} = createModel();
const headerActions: PageHeaderItem[] = [
{ {
icon: 'ti ti-refresh', icon: 'ti ti-refresh',
text: i18n.ts.reload, text: i18n.ts.reload,
handler: () => userRecentNotes.value?.reload(), handler: () => userRecentNotes.value?.reload(),
} satisfies PageHeaderItem, },
{ createOptions(),
icon: 'ti ti-dots',
text: i18n.ts.options,
handler: (ev) => {
os.popupMenu([
{
type: 'switch',
text: i18n.ts.showRenotes,
ref: withRenotes,
}, {
type: 'switch',
text: i18n.ts.showRepliesToOthersInTimeline,
ref: withReplies,
disabled: onlyFiles,
},
{
type: 'divider',
},
{
type: 'switch',
text: i18n.ts.fileAttachedOnly,
ref: onlyFiles,
disabled: withReplies,
},
], ev.currentTarget ?? ev.target);
},
} satisfies PageHeaderItem,
]; ];
// Based on user/index.vue // Based on user/index.vue
@ -83,9 +63,4 @@ definePageMetadata(() => ({
}, },
} : {}, } : {},
})); }));
</script> </script>
<style lang="scss" module>
</style>

View file

@ -0,0 +1,118 @@
/*
* SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { computed } 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';
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 function createOptions(): PageHeaderItem {
const {
userList,
withNonPublic,
withQuotes,
withBots,
withReplies,
onlyFiles,
} = createModel();
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),
};
}
export function createModel() {
const userList = computed({
get: () => defaultStore.reactiveState.followingFeed.value.userList,
set: value => saveFollowingFilter('userList', value),
});
const withNonPublic = computed({
get: () => {
if (userList.value === 'followers') return false;
return defaultStore.reactiveState.followingFeed.value.withNonPublic;
},
set: value => saveFollowingFilter('withNonPublic', value),
});
const withQuotes = computed({
get: () => defaultStore.reactiveState.followingFeed.value.withQuotes,
set: value => saveFollowingFilter('withQuotes', value),
});
const withBots = computed({
get: () => defaultStore.reactiveState.followingFeed.value.withBots,
set: value => saveFollowingFilter('withBots', value),
});
const withReplies = computed({
get: () => defaultStore.reactiveState.followingFeed.value.withReplies,
set: value => saveFollowingFilter('withReplies', value),
});
const onlyFiles = computed({
get: () => defaultStore.reactiveState.followingFeed.value.onlyFiles,
set: value => saveFollowingFilter('onlyFiles', value),
});
const remoteWarningDismissed = computed({
get: () => defaultStore.reactiveState.followingFeed.value.remoteWarningDismissed,
set: value => saveFollowingFilter('remoteWarningDismissed', value),
});
return {
userList,
withNonPublic,
withQuotes,
withBots,
withReplies,
onlyFiles,
remoteWarningDismissed,
};
}
// Based on timeline.saveTlFilter()
function saveFollowingFilter<Key extends keyof typeof defaultStore.state.followingFeed>(key: Key, value: (typeof defaultStore.state.followingFeed)[Key]) {
const out = deepMerge({ [key]: value }, defaultStore.state.followingFeed);
return defaultStore.set('followingFeed', out);
}

View file

@ -11,7 +11,7 @@ import darkTheme from '@@/themes/d-ice.json5';
import { miLocalStorage } from './local-storage.js'; import { miLocalStorage } from './local-storage.js';
import { searchEngineMap } from './scripts/search-engine-map.js'; import { searchEngineMap } from './scripts/search-engine-map.js';
import type { SoundType } from '@/scripts/sound.js'; import type { SoundType } from '@/scripts/sound.js';
import type { FollowingFeedTab } from '@/pages/following-feed.vue'; import type { FollowingFeedTab } from '@/scripts/following-feed-utils.js';
import { Storage } from '@/pizzax.js'; import { Storage } from '@/pizzax.js';
interface PostFormAction { interface PostFormAction {