fix bulk expand mute
This commit is contained in:
parent
40695c7925
commit
4847257011
11 changed files with 201 additions and 73 deletions
|
|
@ -14,7 +14,7 @@ Displays a note with either Misskey or Sharkey style, based on user preference.
|
|||
:withHardMute="withHardMute"
|
||||
@reaction="emoji => emit('reaction', emoji)"
|
||||
@removeReaction="emoji => emit('removeReaction', emoji)"
|
||||
@expandCW="n => emit('expandCW', n)"
|
||||
@expandMute="n => emit('expandMute', n)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
@ -45,6 +45,6 @@ defineProps<{
|
|||
const emit = defineEmits<{
|
||||
(ev: 'reaction', emoji: string): void;
|
||||
(ev: 'removeReaction', emoji: string): void;
|
||||
(ev: 'expandCW', note: Misskey.entities.Note): void;
|
||||
(ev: 'expandMute', note: Misskey.entities.Note): void;
|
||||
}>();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -11,30 +11,61 @@ Displays a note in the detailed view with either Misskey or Sharkey style, based
|
|||
:note="note"
|
||||
:initialTab="initialTab"
|
||||
:expandAllCws="expandAllCws"
|
||||
@expandMute="n => onExpandNote(n)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { computed, defineAsyncComponent, useTemplateRef } from 'vue';
|
||||
import { defineAsyncComponent, useTemplateRef } from 'vue';
|
||||
import type { ComponentExposed } from 'vue-component-type-helpers';
|
||||
import type MkNoteDetailed from '@/components/MkNoteDetailed.vue';
|
||||
import type SkNoteDetailed from '@/components/SkNoteDetailed.vue';
|
||||
import { prefer } from '@/preferences';
|
||||
import { useMuteOverrides } from '@/utility/check-word-mute';
|
||||
import { deepAssign } from '@/utility/merge';
|
||||
|
||||
const XNoteDetailed = defineAsyncComponent(() =>
|
||||
prefer.s.noteDesign === 'misskey'
|
||||
? import('@/components/MkNoteDetailed.vue')
|
||||
: import('@/components/SkNoteDetailed.vue'),
|
||||
);
|
||||
? import('@/components/MkNoteDetailed.vue')
|
||||
: import('@/components/SkNoteDetailed.vue'));
|
||||
|
||||
const rootEl = useTemplateRef<ComponentExposed<typeof MkNoteDetailed | typeof SkNoteDetailed>>('rootEl');
|
||||
const muteOverrides = useMuteOverrides();
|
||||
|
||||
defineExpose({ rootEl });
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
initialTab?: string;
|
||||
expandAllCws?: boolean;
|
||||
}>();
|
||||
|
||||
// TODO map to expand all CWs?
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'expandMute', note: Misskey.entities.Note): void;
|
||||
}>();
|
||||
|
||||
function onExpandNote(note: Misskey.entities.Note) {
|
||||
// Since this is a Detailed note, note.props must point to the top of a thread.
|
||||
// Go ahead and expand matching user/instance/thread mutes downstream, since the user is very likely to want them.
|
||||
if (note.id === props.note.id) {
|
||||
deepAssign(muteOverrides, {
|
||||
user: {
|
||||
[note.user.id]: {
|
||||
userMandatoryCW: null,
|
||||
instanceMandatoryCW: null,
|
||||
},
|
||||
},
|
||||
thread: {
|
||||
[note.threadId]: {
|
||||
threadMuted: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
emit('expandMute', note);
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:withHardMute="withHardMute"
|
||||
:class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]"
|
||||
:tabindex="isDeleted ? '-1' : '0'"
|
||||
@expand="n => emit('expandCW', n)"
|
||||
@expandMute="n => emit('expandMute', n)"
|
||||
>
|
||||
<div v-if="appearNote.reply && inReplyToCollapsed" :class="$style.collapsedInReplyTo">
|
||||
<MkAvatar :class="$style.collapsedInReplyToAvatar" :user="appearNote.reply.user" link preview/>
|
||||
|
|
@ -245,7 +245,7 @@ provide(DI.mock, props.mock);
|
|||
const emit = defineEmits<{
|
||||
(ev: 'reaction', emoji: string): void;
|
||||
(ev: 'removeReaction', emoji: string): void;
|
||||
(ev: 'expandCW', note: Misskey.entities.Note): void;
|
||||
(ev: 'expandMute', note: Misskey.entities.Note): void;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
:note="appearNote"
|
||||
:class="$style.root"
|
||||
:tabindex="isDeleted ? '-1' : '0'"
|
||||
@expandMute="n => emit('expandMute', n)"
|
||||
>
|
||||
<div v-if="appearNote.reply && appearNote.reply.replyId">
|
||||
<div v-if="!conversationLoaded" style="padding: 16px">
|
||||
|
|
@ -292,6 +293,10 @@ const props = withDefaults(defineProps<{
|
|||
initialTab: 'replies',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'expandMute', note: Misskey.entities.Note): void;
|
||||
}>();
|
||||
|
||||
const inChannel = inject('inChannel', null);
|
||||
|
||||
const note = ref(deepClone(props.note));
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #default="{ items: notes }">
|
||||
<div :class="[$style.root, { [$style.noGap]: noGap, '_gaps': !noGap, [$style.reverse]: pagination.reversed }]">
|
||||
<template v-for="(note, i) in notes" :key="note.id">
|
||||
<DynamicNote :class="$style.note" :note="note as Misskey.entities.Note" :withHardMute="true" :data-scroll-anchor="note.id"/>
|
||||
<DynamicNote :class="$style.note" :note="note as Misskey.entities.Note" :withHardMute="true" :data-scroll-anchor="note.id" @expandMute="n => emit('expandMute', n)"/>
|
||||
<MkAd v-if="note._shouldInsertAd_" :preferForms="['horizontal', 'horizontal-big']" :class="$style.ad"/>
|
||||
</template>
|
||||
</div>
|
||||
|
|
@ -37,6 +37,10 @@ const pagingComponent = useTemplateRef('pagingComponent');
|
|||
defineExpose({
|
||||
pagingComponent,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'expandMute', note: Misskey.entities.Note): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
|
|
@ -94,20 +94,19 @@ const props = withDefaults(defineProps<{
|
|||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(type: 'expand', note: Misskey.entities.Note): void;
|
||||
(type: 'expandMute', note: Misskey.entities.Note): void;
|
||||
}>();
|
||||
|
||||
const expandNote = ref(false);
|
||||
|
||||
function expand() {
|
||||
expandNote.value = true;
|
||||
emit('expand', props.note);
|
||||
emit('expandMute', props.note);
|
||||
}
|
||||
|
||||
const mute = computed(() => checkMute(props.note, props.withHardMute));
|
||||
const mute = checkMute(computed(() => props.note), computed(() => props.withHardMute));
|
||||
const mutedWords = computed(() => mute.value.softMutedWords?.join(', '));
|
||||
const isMuted = computed(() => mute.value.hardMuted || mutedWords.value || mute.value.noteMandatoryCW || mute.value.userMandatoryCW || mute.value.instanceMandatoryCW || mute.value.noteMuted || mute.value.threadMuted || mute.value.sensitiveMuted);
|
||||
const isExpanded = computed(() => expandNote.value || !isMuted.value);
|
||||
const isExpanded = computed(() => expandNote.value || !mute.value.hasMute);
|
||||
const rootClass = computed(() => isExpanded.value ? props.expandedClass : undefined);
|
||||
|
||||
const rootEl = useTemplateRef('rootEl');
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ Displays a note in the Sharkey style. Used to show the "main" note in a given co
|
|||
:withHardMute="withHardMute"
|
||||
:class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender }]"
|
||||
:tabindex="isDeleted ? '-1' : '0'"
|
||||
@expand="n => emit('expandCW', n)"
|
||||
@expandMute="n => emit('expandMute', n)"
|
||||
>
|
||||
<SkNoteSub v-if="appearNote.reply" v-show="!renoteCollapsed && !inReplyToCollapsed" :note="appearNote.reply" :class="$style.replyTo"/>
|
||||
<div v-if="appearNote.reply && inReplyToCollapsed && !renoteCollapsed" :class="$style.collapsedInReplyTo">
|
||||
|
|
@ -246,7 +246,7 @@ provide(DI.mock, props.mock);
|
|||
const emit = defineEmits<{
|
||||
(ev: 'reaction', emoji: string): void;
|
||||
(ev: 'removeReaction', emoji: string): void;
|
||||
(ev: 'expandCW', note: Misskey.entities.Note): void;
|
||||
(ev: 'expandMute', note: Misskey.entities.Note): void;
|
||||
}>();
|
||||
|
||||
const router = useRouter();
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ Detailed view of a note in the Sharkey style. Used when opening a note onto its
|
|||
:note="appearNote"
|
||||
:class="$style.root"
|
||||
:tabindex="isDeleted ? '-1' : '0'"
|
||||
@expandMute="n => emit('expandMute', n)"
|
||||
>
|
||||
<div v-if="appearNote.reply && appearNote.reply.replyId && !conversationLoaded" style="padding: 16px">
|
||||
<MkButton style="margin: 0 auto;" primary rounded @click="loadConversation">{{ i18n.ts.loadConversation }}</MkButton>
|
||||
|
|
@ -297,6 +298,10 @@ const props = withDefaults(defineProps<{
|
|||
initialTab: 'replies',
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'expandMute', note: Misskey.entities.Note): void;
|
||||
}>();
|
||||
|
||||
const inChannel = inject('inChannel', null);
|
||||
|
||||
const note = ref(deepClone(props.note));
|
||||
|
|
|
|||
|
|
@ -176,10 +176,10 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkResult type="empty" :text="i18n.ts.noNotes"/>
|
||||
</div>
|
||||
<div v-else class="_panel">
|
||||
<DynamicNote v-for="note of user.pinnedNotes" :key="note.id" class="note" :class="$style.pinnedNote" :note="note" :pinned="true" @expandCW="onExpandCW"/>
|
||||
<DynamicNote v-for="note of user.pinnedNotes" :key="note.id" class="note" :class="$style.pinnedNote" :note="note" :pinned="true" @expandMute="n => onExpandCW(n)"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkNotes v-else :class="$style.tl" :noGap="true" :pagination="AllPagination" @expandCW="onExpandCW"/>
|
||||
<MkNotes v-else :class="$style.tl" :noGap="true" :pagination="AllPagination" @expandMute="n => onExpandCW(n)"/>
|
||||
</MkLazy>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
|
|
@ -198,7 +198,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { getScrollPosition } from '@@/js/scroll.js';
|
||||
import { patchMuteOverrides } from '@/utility/check-word-mute.js';
|
||||
import { useMuteOverrides } from '@/utility/check-word-mute.js';
|
||||
import MkTab from '@/components/MkTab.vue';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||
|
|
@ -224,6 +224,7 @@ import MkSparkle from '@/components/MkSparkle.vue';
|
|||
import { prefer } from '@/preferences.js';
|
||||
import DynamicNote from '@/components/DynamicNote.vue';
|
||||
import MkOmit from '@/components/MkOmit.vue';
|
||||
import { deepAssign } from '@/utility/merge';
|
||||
|
||||
function calcAge(birthdate: string): number {
|
||||
const date = new Date(birthdate);
|
||||
|
|
@ -256,12 +257,20 @@ const emit = defineEmits<{
|
|||
(ev: 'unfoldFiles'): void;
|
||||
}>();
|
||||
|
||||
const cwOverrides = patchMuteOverrides();
|
||||
const muteOverrides = useMuteOverrides();
|
||||
|
||||
function onExpandCW() {
|
||||
// This kills the user-level and instance-level CWs for all notes below this point
|
||||
cwOverrides.userMandatoryCW = null;
|
||||
cwOverrides.instanceMandatoryCW = null;
|
||||
function onExpandCW(note: Misskey.entities.Note) {
|
||||
if (note.user.id === props.user.id) {
|
||||
// This kills the mandatoryCW for this user below this point
|
||||
deepAssign(muteOverrides, {
|
||||
user: {
|
||||
[props.user.id]: {
|
||||
userMandatoryCW: null,
|
||||
instanceMandatoryCW: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const router = useRouter();
|
||||
|
|
|
|||
|
|
@ -4,11 +4,14 @@
|
|||
*/
|
||||
|
||||
import * as Misskey from 'misskey-js';
|
||||
import { provide, inject, reactive } from 'vue';
|
||||
import type { Ref, Reactive } from 'vue';
|
||||
import { provide, inject, reactive, computed } from 'vue';
|
||||
import type { Ref, ComputedRef, Reactive } from 'vue';
|
||||
import { $i } from '@/i.js';
|
||||
import { deepAssign } from '@/utility/merge';
|
||||
|
||||
export interface Mute {
|
||||
hasMute: boolean;
|
||||
|
||||
hardMuted?: boolean;
|
||||
softMutedWords?: string[];
|
||||
sensitiveMuted?: boolean;
|
||||
|
|
@ -23,67 +26,103 @@ export interface Mute {
|
|||
instanceMandatoryCW?: string | null;
|
||||
}
|
||||
|
||||
export interface MuteOverrides {
|
||||
/**
|
||||
* Allows directly modifying the Mute object for all mutes.
|
||||
*/
|
||||
all?: Partial<Omit<Mute, 'hasMute'>>;
|
||||
|
||||
/**
|
||||
* Per instance overrides.
|
||||
* Key: instance hostname.
|
||||
*/
|
||||
instance: Partial<Record<string, Partial<Mute>>>;
|
||||
|
||||
/**
|
||||
* Per user overrides.
|
||||
* Key: user ID.
|
||||
*/
|
||||
user: Partial<Record<string, Partial<Mute>>>;
|
||||
|
||||
/**
|
||||
* Per note overrides.
|
||||
* Key: note ID.
|
||||
*/
|
||||
note: Partial<Record<string, Partial<Mute>>>;
|
||||
|
||||
/**
|
||||
* Per thread overrides.
|
||||
* Key: thread ID.
|
||||
*/
|
||||
thread: Partial<Record<string, Partial<Mute>>>;
|
||||
}
|
||||
|
||||
export const muteOverridesSymbol = Symbol('muteOverrides');
|
||||
|
||||
export function injectMuteOverrides(): Reactive<Partial<Mute>> | null {
|
||||
return inject(muteOverridesSymbol, null);
|
||||
}
|
||||
export function useMuteOverrides(): Reactive<MuteOverrides> {
|
||||
// Re-use the same instance if possible
|
||||
let overrides = injectMuteOverrides();
|
||||
|
||||
export function provideMuteOverrides(overrides: Reactive<Partial<Mute>> | null) {
|
||||
provide(muteOverridesSymbol, overrides);
|
||||
}
|
||||
|
||||
export function patchMuteOverrides(patch?: Partial<Mute>): Reactive<Partial<Mute>> {
|
||||
// Inject and re-provide to merge with any overrides injected from above
|
||||
const overrides = injectMuteOverrides() ?? reactive({});
|
||||
provideMuteOverrides(overrides);
|
||||
|
||||
// Assign caller's changes, if any
|
||||
if (patch) {
|
||||
Object.assign(overrides, patch);
|
||||
if (!overrides) {
|
||||
overrides = reactive({
|
||||
note: {},
|
||||
user: {},
|
||||
instance: {},
|
||||
thread: {},
|
||||
});
|
||||
provideMuteOverrides(overrides);
|
||||
}
|
||||
|
||||
return overrides;
|
||||
}
|
||||
|
||||
export function checkMute(note: Misskey.entities.Note, withHardMute?: boolean): Mute {
|
||||
const mutes = getMutes(note, withHardMute);
|
||||
|
||||
const override = injectMuteOverrides();
|
||||
if (override) {
|
||||
Object.assign(mutes, override);
|
||||
}
|
||||
|
||||
return mutes;
|
||||
function injectMuteOverrides(): Reactive<MuteOverrides> | null {
|
||||
return inject(muteOverridesSymbol, null);
|
||||
}
|
||||
|
||||
function getMutes(note: Misskey.entities.Note, withHardMute?: boolean): Mute {
|
||||
const sensitiveMuted = isSensitiveMuted(note);
|
||||
function provideMuteOverrides(overrides: Reactive<MuteOverrides> | null) {
|
||||
provide(muteOverridesSymbol, overrides);
|
||||
}
|
||||
|
||||
// My own note
|
||||
if ($i && $i.id === note.userId) {
|
||||
return { sensitiveMuted };
|
||||
}
|
||||
export function checkMute(note: ComputedRef<Misskey.entities.Note>, withHardMute?: ComputedRef<boolean>): ComputedRef<Mute> {
|
||||
// inject() can only be used inside script setup, so it MUST be outside the computed block!
|
||||
const overrides = injectMuteOverrides();
|
||||
|
||||
const threadMuted = note.isMutingThread;
|
||||
const noteMuted = note.isMutingNote;
|
||||
const noteMandatoryCW = note.mandatoryCW;
|
||||
const userMandatoryCW = note.user.mandatoryCW;
|
||||
const instanceMandatoryCW = note.user.instance?.mandatoryCW;
|
||||
return computed(() => getMutes(note.value, withHardMute?.value ?? true, overrides));
|
||||
}
|
||||
|
||||
// Hard mute
|
||||
if (withHardMute && isHardMuted(note)) {
|
||||
return { hardMuted: true, sensitiveMuted, threadMuted, noteMuted, noteMandatoryCW, userMandatoryCW, instanceMandatoryCW };
|
||||
}
|
||||
function getMutes(note: Misskey.entities.Note, withHardMute: boolean, overrides: MuteOverrides | null): Mute {
|
||||
const override: Partial<Mute> = overrides ? deepAssign(
|
||||
{},
|
||||
note.user.host ? overrides.instance[note.user.host] : undefined,
|
||||
overrides.user[note.user.id],
|
||||
overrides.thread[note.threadId],
|
||||
overrides.note[note.id],
|
||||
overrides.all,
|
||||
) : {};
|
||||
|
||||
// Soft mute
|
||||
const softMutedWords = isSoftMuted(note);
|
||||
if (softMutedWords.length > 0) {
|
||||
return { softMutedWords, sensitiveMuted, threadMuted, noteMuted, noteMandatoryCW, userMandatoryCW, instanceMandatoryCW };
|
||||
}
|
||||
const isMe = $i != null && $i.id === note.userId;
|
||||
|
||||
// Other / no mute
|
||||
return { sensitiveMuted, threadMuted, noteMuted, noteMandatoryCW, userMandatoryCW, instanceMandatoryCW };
|
||||
const hardMuted = override.hardMuted ?? (!isMe && withHardMute && isHardMuted(note));
|
||||
const softMutedWords = override.softMutedWords ?? (isMe ? [] : isSoftMuted(note));
|
||||
const sensitiveMuted = override.sensitiveMuted ?? isSensitiveMuted(note);
|
||||
const threadMuted = override.threadMuted ?? (!isMe && note.isMutingThread);
|
||||
const noteMuted = override.noteMuted ?? (!isMe && note.isMutingNote);
|
||||
const noteMandatoryCW = override.noteMandatoryCW !== undefined
|
||||
? override.noteMandatoryCW
|
||||
: (isMe ? null : note.mandatoryCW);
|
||||
const userMandatoryCW = override.userMandatoryCW !== undefined
|
||||
? override.userMandatoryCW
|
||||
: (isMe ? null : note.user.mandatoryCW);
|
||||
const instanceMandatoryCW = override.instanceMandatoryCW !== undefined
|
||||
? override.instanceMandatoryCW
|
||||
: (!isMe && note.user.instance)
|
||||
? note.user.instance.mandatoryCW
|
||||
: null;
|
||||
|
||||
const hasMute = hardMuted || softMutedWords.length > 0 || sensitiveMuted || threadMuted || noteMuted || !!noteMandatoryCW || !!userMandatoryCW || !!instanceMandatoryCW;
|
||||
|
||||
return { hasMute, hardMuted, softMutedWords, sensitiveMuted, threadMuted, noteMuted, noteMandatoryCW, userMandatoryCW, instanceMandatoryCW };
|
||||
}
|
||||
|
||||
function isHardMuted(note: Misskey.entities.Note): boolean {
|
||||
|
|
|
|||
|
|
@ -33,3 +33,39 @@ export function deepMerge<X extends Record<PropertyKey, unknown>>(value: DeepPar
|
|||
}
|
||||
throw new Error('deepMerge: value and def must be pure objects');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns properties from one or more partial objects into a target.
|
||||
* Nested objects are assigned in the same way.
|
||||
* Like Object.assign, but deep.
|
||||
*/
|
||||
export function deepAssign<T extends Record<PropertyKey, unknown>>(target: T, ...partials: (DeepPartial<T> | undefined)[]): T {
|
||||
return _deepAssign(target, ...partials) as T;
|
||||
}
|
||||
|
||||
function _deepAssign(target: Record<PropertyKey, unknown>, ...partials: (Record<PropertyKey, unknown> | undefined)[]): Record<PropertyKey, unknown> {
|
||||
if (isPureObject(target)) {
|
||||
for (const partial of partials) {
|
||||
if (!isPureObject(partial)) continue;
|
||||
|
||||
for (const [key, value] of Object.entries(partial)) {
|
||||
// Populate empty keys
|
||||
if (!Reflect.has(target, key)) {
|
||||
target[key] = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Merge objects
|
||||
if (isPureObject(target[key]) && isPureObject(value)) {
|
||||
_deepAssign(target[key], value);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Replace flat values
|
||||
target[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue