forked from Mirrors/elk
feat: totally hide strict filters (#948)
This commit is contained in:
parent
1a7ae6f0ef
commit
a08f56676d
18 changed files with 82 additions and 33 deletions
|
@ -8,3 +8,4 @@ Dockerfile
|
||||||
public/
|
public/
|
||||||
https-dev-config/localhost.crt
|
https-dev-config/localhost.crt
|
||||||
https-dev-config/localhost.key
|
https-dev-config/localhost.key
|
||||||
|
Dockerfile
|
||||||
|
|
|
@ -4,10 +4,16 @@ import type { Paginator, mastodon } from 'masto'
|
||||||
const { paginator } = defineProps<{
|
const { paginator } = defineProps<{
|
||||||
paginator: Paginator<mastodon.v1.Conversation[], mastodon.DefaultPaginationParams>
|
paginator: Paginator<mastodon.v1.Conversation[], mastodon.DefaultPaginationParams>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
function preprocess(items: mastodon.v1.Conversation[]): mastodon.v1.Conversation[] {
|
||||||
|
return items.filter(items => !items.lastStatus?.filtered?.find(
|
||||||
|
filter => filter.filter.filterAction === 'hide' && filter.filter.context.includes('thread'),
|
||||||
|
))
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CommonPaginator :paginator="paginator">
|
<CommonPaginator :paginator="paginator" :preprocess="preprocess">
|
||||||
<template #default="{ item }">
|
<template #default="{ item }">
|
||||||
<ConversationCard
|
<ConversationCard
|
||||||
:conversation="item"
|
:conversation="item"
|
||||||
|
|
|
@ -112,6 +112,12 @@ function groupItems(items: mastodon.v1.Notification[]): NotificationSlot[] {
|
||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeFiltered(items: mastodon.v1.Notification[]): mastodon.v1.Notification[] {
|
||||||
|
return items.filter(item => !item.status?.filtered?.find(
|
||||||
|
filter => filter.filter.filterAction === 'hide' && filter.filter.context.includes('notifications'),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
function preprocess(items: NotificationSlot[]): NotificationSlot[] {
|
function preprocess(items: NotificationSlot[]): NotificationSlot[] {
|
||||||
const flattenedNotifications: mastodon.v1.Notification[] = []
|
const flattenedNotifications: mastodon.v1.Notification[] = []
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
@ -131,7 +137,7 @@ function preprocess(items: NotificationSlot[]): NotificationSlot[] {
|
||||||
flattenedNotifications.push(item)
|
flattenedNotifications.push(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return groupItems(flattenedNotifications)
|
return groupItems(removeFiltered(flattenedNotifications))
|
||||||
}
|
}
|
||||||
|
|
||||||
const { clearNotifications } = useNotifications()
|
const { clearNotifications } = useNotifications()
|
||||||
|
|
|
@ -67,13 +67,6 @@ const createdAt = useFormattedDateTime(status.createdAt)
|
||||||
const timeAgoOptions = useTimeAgoOptions(true)
|
const timeAgoOptions = useTimeAgoOptions(true)
|
||||||
const timeago = useTimeAgo(() => status.createdAt, timeAgoOptions)
|
const timeago = useTimeAgo(() => status.createdAt, timeAgoOptions)
|
||||||
|
|
||||||
// Content Filter logic
|
|
||||||
const filterResult = $computed(() => status.filtered?.length ? status.filtered[0] : null)
|
|
||||||
const filter = $computed(() => filterResult?.filter)
|
|
||||||
|
|
||||||
const filterPhrase = $computed(() => filter?.title)
|
|
||||||
const isFiltered = $computed(() => filterPhrase && (props.context ? filter?.context.includes(props.context) : false))
|
|
||||||
|
|
||||||
const isSelfReply = $computed(() => status.inReplyToAccountId === status.account.id)
|
const isSelfReply = $computed(() => status.inReplyToAccountId === status.account.id)
|
||||||
const collapseRebloggedBy = $computed(() => rebloggedBy?.id === status.account.id)
|
const collapseRebloggedBy = $computed(() => rebloggedBy?.id === status.account.id)
|
||||||
const isDM = $computed(() => status.visibility === 'direct')
|
const isDM = $computed(() => status.visibility === 'direct')
|
||||||
|
@ -84,7 +77,6 @@ const showReplyTo = $computed(() => !replyToMain && !directReply)
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="filter?.filterAction !== 'hide'"
|
|
||||||
:id="`status-${status.id}`"
|
:id="`status-${status.id}`"
|
||||||
ref="el"
|
ref="el"
|
||||||
relative flex="~ col gap1" p="l-3 r-4 b-2"
|
relative flex="~ col gap1" p="l-3 r-4 b-2"
|
||||||
|
@ -189,9 +181,4 @@ const showReplyTo = $computed(() => !replyToMain && !directReply)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="isFiltered" gap-2 p-4 :class="{ 'border-t border-base': newer }">
|
|
||||||
<p text-center text-secondary text-sm>
|
|
||||||
{{ filterPhrase && `${$t('status.filter_removed_phrase')}: ${filterPhrase}` }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -27,7 +27,7 @@ const isFiltered = $computed(() => filterPhrase && (context && context !== 'deta
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<StatusBody v-if="!isFiltered && status.sensitive && !status.spoilerText" :status="status" :newer="newer" :with-action="!isDetails" :class="isDetails ? 'text-xl' : ''" />
|
<StatusBody v-if="!isFiltered && status.sensitive && !status.spoilerText" :status="status" :newer="newer" :with-action="!isDetails" :class="isDetails ? 'text-xl' : ''" />
|
||||||
<StatusSpoiler :enabled="status.sensitive || isFiltered" :filter="isFiltered">
|
<StatusSpoiler :enabled="status.sensitive || isFiltered" :filter="isFiltered" :is-d-m="isDM">
|
||||||
<template v-if="filterPhrase" #spoiler>
|
<template v-if="filterPhrase" #spoiler>
|
||||||
<p>{{ `${$t('status.filter_hidden_phrase')}: ${filterPhrase}` }}</p>
|
<p>{{ `${$t('status.filter_hidden_phrase')}: ${filterPhrase}` }}</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const props = defineProps<{ enabled?: boolean; filter?: boolean }>()
|
const props = defineProps<{ enabled?: boolean; filter?: boolean; isDM?: boolean }>()
|
||||||
|
|
||||||
const showContent = ref(!props.enabled)
|
const showContent = ref(!props.enabled)
|
||||||
const toggleContent = useToggle(showContent)
|
const toggleContent = useToggle(showContent)
|
||||||
|
@ -14,8 +14,8 @@ watchEffect(() => {
|
||||||
<div class="content-rich" p="x-4 b-2.5" text-center text-secondary w-full border="~ base" border-0 border-b-dotted border-b-3 mt-2>
|
<div class="content-rich" p="x-4 b-2.5" text-center text-secondary w-full border="~ base" border-0 border-b-dotted border-b-3 mt-2>
|
||||||
<slot name="spoiler" />
|
<slot name="spoiler" />
|
||||||
</div>
|
</div>
|
||||||
<div flex="~ gap-1 center" w-full mt="-4.5">
|
<div flex="~ gap-1 center" w-full :mb="isDM && !showContent ? '4' : ''" mt="-4.5">
|
||||||
<button btn-text px-2 py-1 bg-base flex="~ center gap-2" :class="showContent ? '' : 'filter-saturate-0 hover:filter-saturate-100'" @click="toggleContent()">
|
<button btn-text px-2 py-1 :bg="isDM ? 'transparent' : 'base'" flex="~ center gap-2" :class="showContent ? '' : 'filter-saturate-0 hover:filter-saturate-100'" @click="toggleContent()">
|
||||||
<div v-if="showContent" i-ri:eye-line />
|
<div v-if="showContent" i-ri:eye-line />
|
||||||
<div v-else i-ri:eye-close-line />
|
<div v-else i-ri:eye-close-line />
|
||||||
{{ showContent ? $t('status.spoiler_show_less') : $t(filter ? 'status.filter_show_anyway' : 'status.spoiler_show_more') }}
|
{{ showContent ? $t('status.spoiler_show_less') : $t(filter ? 'status.filter_show_anyway' : 'status.spoiler_show_more') }}
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { mastodon } from 'masto'
|
||||||
|
|
||||||
const paginator = useMastoClient().v1.timelines.listHome({ limit: 30 })
|
const paginator = useMastoClient().v1.timelines.listHome({ limit: 30 })
|
||||||
const stream = $(useStreaming(client => client.v1.stream.streamUser()))
|
const stream = $(useStreaming(client => client.v1.stream.streamUser()))
|
||||||
|
const reorderAndFilter = (items: mastodon.v1.Status[]) => reorderedTimeline(items, 'home')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<PublishWidget draft-key="home" border="b base" />
|
<PublishWidget draft-key="home" border="b base" />
|
||||||
<TimelinePaginator v-bind="{ paginator, stream }" :preprocess="reorderedTimeline" context="home" />
|
<TimelinePaginator v-bind="{ paginator, stream }" :preprocess="reorderAndFilter" context="home" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { mastodon } from 'masto'
|
||||||
|
|
||||||
const paginator = useMastoClient().v1.timelines.listPublic({ limit: 30 })
|
const paginator = useMastoClient().v1.timelines.listPublic({ limit: 30 })
|
||||||
const stream = useStreaming(client => client.v1.stream.streamPublicTimeline())
|
const stream = useStreaming(client => client.v1.stream.streamPublicTimeline())
|
||||||
|
const reorderAndFilter = (items: mastodon.v1.Status[]) => reorderedTimeline(items, 'public')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<TimelinePaginator v-bind="{ paginator, stream }" context="public" />
|
<TimelinePaginator v-bind="{ paginator, stream }" :preprocess="reorderAndFilter" context="public" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -9,10 +9,20 @@ function areStatusesConsecutive(a: mastodon.v1.Status, b: mastodon.v1.Status) {
|
||||||
return !!inReplyToId && (inReplyToId === a.reblog?.id || inReplyToId === a.id)
|
return !!inReplyToId && (inReplyToId === a.reblog?.id || inReplyToId === a.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function reorderedTimeline(items: mastodon.v1.Status[]) {
|
function removeFilteredItems(items: mastodon.v1.Status[], context: mastodon.v1.FilterContext): mastodon.v1.Status[] {
|
||||||
|
const isStrict = (filter: mastodon.v1.FilterResult) => filter.filter.filterAction === 'hide' && filter.filter.context.includes(context)
|
||||||
|
const isFiltered = (item: mastodon.v1.Status) => !item.filtered?.find(isStrict)
|
||||||
|
const isReblogFiltered = (item: mastodon.v1.Status) => !item.reblog?.filtered?.find(isStrict)
|
||||||
|
|
||||||
|
return [...items].filter(isFiltered).filter(isReblogFiltered)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reorderedTimeline(items: mastodon.v1.Status[], context: mastodon.v1.FilterContext = 'public') {
|
||||||
let steps = 0
|
let steps = 0
|
||||||
const newItems = [...items]
|
|
||||||
for (let i = items.length - 1; i > 0; i--) {
|
const newItems = removeFilteredItems(items, context)
|
||||||
|
|
||||||
|
for (let i = newItems.length - 1; i > 0; i--) {
|
||||||
for (let k = 1; k <= maxDistance && i - k >= 0; k++) {
|
for (let k = 1; k <= maxDistance && i - k >= 0; k++) {
|
||||||
// Prevent infinite loops
|
// Prevent infinite loops
|
||||||
steps++
|
steps++
|
||||||
|
|
|
@ -158,7 +158,6 @@
|
||||||
"status": {
|
"status": {
|
||||||
"edited": "Upraveno {0}",
|
"edited": "Upraveno {0}",
|
||||||
"filter_hidden_phrase": "Vyfiltrováno",
|
"filter_hidden_phrase": "Vyfiltrováno",
|
||||||
"filter_removed_phrase": "Vyfiltrováno",
|
|
||||||
"filter_show_anyway": "Ukázat i tak",
|
"filter_show_anyway": "Ukázat i tak",
|
||||||
"img_alt": {
|
"img_alt": {
|
||||||
"desc": "Popis",
|
"desc": "Popis",
|
||||||
|
|
|
@ -377,7 +377,6 @@
|
||||||
"edited": "Zuletzt bearbeitet: {0}",
|
"edited": "Zuletzt bearbeitet: {0}",
|
||||||
"favourited_by": "Favorisiert von",
|
"favourited_by": "Favorisiert von",
|
||||||
"filter_hidden_phrase": "Versteckt durch",
|
"filter_hidden_phrase": "Versteckt durch",
|
||||||
"filter_removed_phrase": "Entfernt durch Filter",
|
|
||||||
"filter_show_anyway": "Trotzdem zeigen",
|
"filter_show_anyway": "Trotzdem zeigen",
|
||||||
"img_alt": {
|
"img_alt": {
|
||||||
"desc": "Beschreibung",
|
"desc": "Beschreibung",
|
||||||
|
|
|
@ -419,7 +419,6 @@
|
||||||
"edited": "Edité {0}",
|
"edited": "Edité {0}",
|
||||||
"favourited_by": "Aimé par",
|
"favourited_by": "Aimé par",
|
||||||
"filter_hidden_phrase": "Filtré par",
|
"filter_hidden_phrase": "Filtré par",
|
||||||
"filter_removed_phrase": "Caché par le filtre",
|
|
||||||
"filter_show_anyway": "Montrer coûte que coûte",
|
"filter_show_anyway": "Montrer coûte que coûte",
|
||||||
"img_alt": {
|
"img_alt": {
|
||||||
"desc": "Description",
|
"desc": "Description",
|
||||||
|
|
|
@ -295,7 +295,6 @@
|
||||||
"status": {
|
"status": {
|
||||||
"edited": "Aangepast {0}",
|
"edited": "Aangepast {0}",
|
||||||
"filter_hidden_phrase": "Gefilterd door",
|
"filter_hidden_phrase": "Gefilterd door",
|
||||||
"filter_removed_phrase": "Verwijderd door filter",
|
|
||||||
"filter_show_anyway": "Laat toch zien",
|
"filter_show_anyway": "Laat toch zien",
|
||||||
"img_alt": {
|
"img_alt": {
|
||||||
"desc": "Omschrijving",
|
"desc": "Omschrijving",
|
||||||
|
|
|
@ -315,7 +315,6 @@
|
||||||
"status": {
|
"status": {
|
||||||
"edited": "Редаговано {0}",
|
"edited": "Редаговано {0}",
|
||||||
"filter_hidden_phrase": "Відфільтровано",
|
"filter_hidden_phrase": "Відфільтровано",
|
||||||
"filter_removed_phrase": "Видалено фільтром",
|
|
||||||
"filter_show_anyway": "Показати все одно",
|
"filter_show_anyway": "Показати все одно",
|
||||||
"img_alt": {
|
"img_alt": {
|
||||||
"desc": "Опис зображення",
|
"desc": "Опис зображення",
|
||||||
|
|
|
@ -378,7 +378,6 @@
|
||||||
"edited": "在 {0} 编辑了",
|
"edited": "在 {0} 编辑了",
|
||||||
"favourited_by": "被喜欢",
|
"favourited_by": "被喜欢",
|
||||||
"filter_hidden_phrase": "筛选依据",
|
"filter_hidden_phrase": "筛选依据",
|
||||||
"filter_removed_phrase": "从筛选中移除",
|
|
||||||
"filter_show_anyway": "仍然展示",
|
"filter_show_anyway": "仍然展示",
|
||||||
"img_alt": {
|
"img_alt": {
|
||||||
"desc": "描述",
|
"desc": "描述",
|
||||||
|
|
|
@ -394,7 +394,6 @@
|
||||||
"edited": "在 {0} 編輯了",
|
"edited": "在 {0} 編輯了",
|
||||||
"favourited_by": "被喜歡",
|
"favourited_by": "被喜歡",
|
||||||
"filter_hidden_phrase": "篩選依據",
|
"filter_hidden_phrase": "篩選依據",
|
||||||
"filter_removed_phrase": "從篩選中移除",
|
|
||||||
"filter_show_anyway": "仍然顯示",
|
"filter_show_anyway": "仍然顯示",
|
||||||
"img_alt": {
|
"img_alt": {
|
||||||
"desc": "描述",
|
"desc": "描述",
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import type { mastodon } from 'masto'
|
||||||
|
|
||||||
const params = useRoute().params
|
const params = useRoute().params
|
||||||
const handle = $(computedEager(() => params.account as string))
|
const handle = $(computedEager(() => params.account as string))
|
||||||
|
|
||||||
|
@ -8,6 +10,8 @@ const { t } = useI18n()
|
||||||
|
|
||||||
const account = await fetchAccountByHandle(handle)
|
const account = await fetchAccountByHandle(handle)
|
||||||
|
|
||||||
|
const reorderAndFilter = (items: mastodon.v1.Status[]) => reorderedTimeline(items, 'account')
|
||||||
|
|
||||||
const paginator = useMastoClient().v1.accounts.listStatuses(account.id, { limit: 30, excludeReplies: true })
|
const paginator = useMastoClient().v1.accounts.listStatuses(account.id, { limit: 30, excludeReplies: true })
|
||||||
|
|
||||||
if (account) {
|
if (account) {
|
||||||
|
@ -20,6 +24,6 @@ if (account) {
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<AccountTabs />
|
<AccountTabs />
|
||||||
<TimelinePaginator :paginator="paginator" :preprocess="reorderedTimeline" context="account" :account="account" />
|
<TimelinePaginator :paginator="paginator" :preprocess="reorderAndFilter" context="account" :account="account" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,7 +1,21 @@
|
||||||
import type { mastodon } from 'masto'
|
import type { mastodon } from 'masto'
|
||||||
import { describe, expect, it } from 'vitest'
|
import { describe, expect, it } from 'vitest'
|
||||||
|
|
||||||
function status(id: string): mastodon.v1.Status {
|
function status(id: string, filtered?: mastodon.v1.FilterContext): mastodon.v1.Status {
|
||||||
|
if (filtered) {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
filtered: [
|
||||||
|
{
|
||||||
|
filter: {
|
||||||
|
filterAction: 'hide',
|
||||||
|
context: [filtered],
|
||||||
|
},
|
||||||
|
} as mastodon.v1.FilterResult,
|
||||||
|
],
|
||||||
|
} as mastodon.v1.Status
|
||||||
|
}
|
||||||
|
|
||||||
return { id } as mastodon.v1.Status
|
return { id } as mastodon.v1.Status
|
||||||
}
|
}
|
||||||
function reply(id: string, s: mastodon.v1.Status) {
|
function reply(id: string, s: mastodon.v1.Status) {
|
||||||
|
@ -26,6 +40,11 @@ const r_b1 = reblog('r_b1', p_b1)
|
||||||
const r_a2 = reblog('r_a2', p_a2)
|
const r_a2 = reblog('r_a2', p_a2)
|
||||||
const r_b2 = reblog('r_b2', p_b2)
|
const r_b2 = reblog('r_b2', p_b2)
|
||||||
|
|
||||||
|
const f1 = status('f1', 'public')
|
||||||
|
const r_f1 = reply('r_f1', f1)
|
||||||
|
const rb_f1 = reblog('rb_f1', f1)
|
||||||
|
const n_f2 = status('f2', 'notifications')
|
||||||
|
|
||||||
describe('timeline reordering', () => {
|
describe('timeline reordering', () => {
|
||||||
it('reorder basic', () => {
|
it('reorder basic', () => {
|
||||||
expect(reorderedTimeline([r_a2, r_a1])).toEqual([r_a1, r_a2])
|
expect(reorderedTimeline([r_a2, r_a1])).toEqual([r_a1, r_a2])
|
||||||
|
@ -48,4 +67,21 @@ describe('timeline reordering', () => {
|
||||||
|
|
||||||
expect(reorderedTimeline([p_a3, r_a1, r_a2, r_b2, p_b3, r_b1])).toEqual([r_a1, r_a2, p_a3, r_b1, r_b2, p_b3])
|
expect(reorderedTimeline([p_a3, r_a1, r_a2, r_b2, p_b3, r_b1])).toEqual([r_a1, r_a2, p_a3, r_b1, r_b2, p_b3])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('reorder with filtered item', () => {
|
||||||
|
// should not show filtered status with 'hide' filterAction
|
||||||
|
expect(reorderedTimeline([p_a3, f1, r_a1, r_a2, r_b2, p_b3, r_b1])).toEqual([r_a1, r_a2, p_a3, r_b1, r_b2, p_b3])
|
||||||
|
|
||||||
|
// should not filter status with 'hide' filterAction but does not match context
|
||||||
|
expect(reorderedTimeline([p_a3, n_f2, r_a1, r_a2, r_b2, p_b3, r_b1], 'public')).toEqual([r_a1, r_a2, p_a3, n_f2, r_b1, r_b2, p_b3])
|
||||||
|
|
||||||
|
// should filter status with 'hide' filterAction and matches context
|
||||||
|
expect(reorderedTimeline([p_a3, n_f2, r_a1, r_a2, r_b2, p_b3, r_b1], 'notifications')).toEqual([r_a1, r_a2, p_a3, r_b1, r_b2, p_b3])
|
||||||
|
|
||||||
|
// should show reply to a filtered status
|
||||||
|
expect(reorderedTimeline([p_a3, f1, r_a1, r_f1, r_a2, r_b2, p_b3, r_b1])).toEqual([r_a1, r_a2, p_a3, r_f1, r_b1, r_b2, p_b3])
|
||||||
|
|
||||||
|
// should not show reblogged status that is filtered with 'hide' filterAction
|
||||||
|
expect(reorderedTimeline([p_a3, f1, r_a1, rb_f1, r_a2, r_b2, p_b3, r_b1])).toEqual([r_a1, r_a2, p_a3, r_b1, r_b2, p_b3])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue