diff options
| author | dakkar <dakkar@thenautilus.net> | 2024-11-23 10:41:33 +0000 |
|---|---|---|
| committer | dakkar <dakkar@thenautilus.net> | 2024-11-23 10:41:33 +0000 |
| commit | 6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7 (patch) | |
| tree | 307fcabbac6985abb8d59fa5c16ce021d1c1c7c4 /packages/frontend/src/components | |
| parent | fix some lints for frontend (diff) | |
| parent | merge: Move `cypress` to `optionalDependencies` (!697) (diff) | |
| download | sharkey-6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7.tar.gz sharkey-6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7.tar.bz2 sharkey-6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7.zip | |
Merge branch 'develop' into feature/2024.10
Diffstat (limited to 'packages/frontend/src/components')
4 files changed, 234 insertions, 1 deletions
diff --git a/packages/frontend/src/components/SkFollowingRecentNotes.vue b/packages/frontend/src/components/SkFollowingRecentNotes.vue new file mode 100644 index 0000000000..6daa8feba5 --- /dev/null +++ b/packages/frontend/src/components/SkFollowingRecentNotes.vue @@ -0,0 +1,144 @@ +<!-- +SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkPullToRefresh :refresher="() => reload()"> + <MkPagination ref="latestNotesPaging" :pagination="latestNotesPagination" @init="onListReady"> + <template #empty> + <div class="_fullinfo"> + <img :src="infoImageUrl" class="_ghost" :alt="i18n.ts.noNotes" aria-hidden="true"/> + <div>{{ i18n.ts.noNotes }}</div> + </div> + </template> + + <template #default="{ items: notes }"> + <MkDateSeparatedList v-slot="{ item: note }" :items="notes" :class="$style.panel" :noGap="true"> + <SkFollowingFeedEntry v-if="!isHardMuted(note)" :isMuted="isSoftMuted(note)" :note="note" :class="props.selectedUserId == note.userId && $style.selected" @select="u => selectUser(u.id)"/> + </MkDateSeparatedList> + </template> + </MkPagination> +</MkPullToRefresh> +</template> + +<script setup lang="ts"> +import * as Misskey from 'misskey-js'; +import { computed, shallowRef } from 'vue'; +import { infoImageUrl } from '@/instance.js'; +import { i18n } from '@/i18n.js'; +import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; +import MkPagination, { Paging } from '@/components/MkPagination.vue'; +import SkFollowingFeedEntry from '@/components/SkFollowingFeedEntry.vue'; +import { $i } from '@/account.js'; +import { checkWordMute } from '@/scripts/check-word-mute.js'; +import { FollowingFeedTab } from '@/scripts/following-feed-utils.js'; +import MkPullToRefresh from '@/components/MkPullToRefresh.vue'; + +const props = defineProps<{ + userList: FollowingFeedTab; + withNonPublic: boolean; + withQuotes: boolean; + withReplies: boolean; + withBots: boolean; + onlyFiles: boolean; + selectedUserId?: string | null; +}>(); + +const emit = defineEmits<{ + (event: 'loaded', initialUserId?: string): void; + (event: 'userSelected', userId: string): void; +}>(); + +defineExpose({ reload }); + +async function reload() { + await latestNotesPaging.value?.reload(); +} + +function selectUser(userId: string) { + emit('userSelected', userId); +} + +async function onListReady(): Promise<void> { + // This looks complicated, but it's really just a trick to get the first user ID from the pagination. + const initialUserId = latestNotesPaging.value?.items.size + ? latestNotesPaging.value.items.values().next().value?.userId + : undefined; + + emit('loaded', initialUserId); +} + +const latestNotesPagination: Paging<'notes/following'> = { + endpoint: 'notes/following' as const, + limit: 20, + params: computed(() => ({ + list: props.userList, + filesOnly: props.onlyFiles, + includeNonPublic: props.withNonPublic, + includeReplies: props.withReplies, + includeQuotes: props.withQuotes, + includeBots: props.withBots, + })), +}; + +const latestNotesPaging = shallowRef<InstanceType<typeof MkPagination>>(); + +function isSoftMuted(note: Misskey.entities.Note): boolean { + return isMuted(note, $i?.mutedWords); +} + +function isHardMuted(note: Misskey.entities.Note): boolean { + return isMuted(note, $i?.hardMutedWords); +} + +// Match the typing used by Misskey +type Mutes = (string | string[])[] | null | undefined; + +// Adapted from MkNote.ts +function isMuted(note: Misskey.entities.Note, mutes: Mutes): boolean { + return checkMute(note, mutes) + || checkMute(note.reply, mutes) + || checkMute(note.renote, mutes); +} + +// Adapted from check-word-mute.ts +function checkMute(note: Misskey.entities.Note | undefined | null, mutes: Mutes): boolean { + if (!note) { + return false; + } + + if (!mutes || mutes.length < 1) { + return false; + } + + return checkWordMute(note, $i, mutes); +} +</script> + +<style module lang="scss"> +.panel { + background: var(--panel); +} + +@keyframes border { + from { + border-left: 0 solid var(--accent); + } + to { + border-left: 6px solid var(--accent); + } +} + +.selected { + animation: border 0.2s ease-out 0s 1 forwards; + + &:first-child { + border-top-left-radius: 5px; + } + + &:last-child { + border-bottom-left-radius: 5px; + } +} +</style> diff --git a/packages/frontend/src/components/SkRemoteFollowersWarning.vue b/packages/frontend/src/components/SkRemoteFollowersWarning.vue new file mode 100644 index 0000000000..ceebbd59dd --- /dev/null +++ b/packages/frontend/src/components/SkRemoteFollowersWarning.vue @@ -0,0 +1,32 @@ +<!-- +SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkInfo v-if="showRemoteWarning" warn closable @close="close"> + {{ i18n.ts.remoteFollowersWarning }} +</MkInfo> +</template> + +<script setup lang="ts"> +import { computed } from 'vue'; +import { i18n } from '@/i18n.js'; +import MkInfo from '@/components/MkInfo.vue'; +import { followersTab, FollowingFeedModel } from '@/scripts/following-feed-utils.js'; + +const props = defineProps<{ + model: FollowingFeedModel, +}>(); + +// eslint-disable-next-line vue/no-setup-props-reactivity-loss +const { model: { userList, remoteWarningDismissed } } = props; + +const showRemoteWarning = computed( + () => userList.value === followersTab && !remoteWarningDismissed.value, +); + +function close() { + remoteWarningDismissed.value = true; +} +</script> diff --git a/packages/frontend/src/components/SkUserRecentNotes.vue b/packages/frontend/src/components/SkUserRecentNotes.vue index f355facb51..908affcdaf 100644 --- a/packages/frontend/src/components/SkUserRecentNotes.vue +++ b/packages/frontend/src/components/SkUserRecentNotes.vue @@ -101,7 +101,7 @@ onMounted(async () => { margin-bottom: 12px; } -@container (min-width: 451px) { +@container (min-width: 750px) { .userInfo { margin-bottom: 24px; } diff --git a/packages/frontend/src/components/global/SkLazy.vue b/packages/frontend/src/components/global/SkLazy.vue new file mode 100644 index 0000000000..40add97db7 --- /dev/null +++ b/packages/frontend/src/components/global/SkLazy.vue @@ -0,0 +1,57 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<!-- Based on MkLazy.vue --> + +<template> +<div ref="rootEl" :class="$style.root"> + <slot v-if="showing"></slot> + <div v-else :class="$style.placeholder"></div> +</div> +</template> + +<script lang="ts" setup> +import { nextTick, onMounted, onActivated, onBeforeUnmount, ref, shallowRef } from 'vue'; + +const rootEl = shallowRef<HTMLDivElement>(); +const showing = ref(false); + +defineExpose({ rootEl, showing }); + +const observer = new IntersectionObserver(entries => + showing.value = entries.some((entry) => entry.isIntersecting), +); + +onMounted(() => { + nextTick(() => { + if (rootEl.value) { + observer.observe(rootEl.value); + } + }); +}); + +onActivated(() => { + nextTick(() => { + if (rootEl.value) { + observer.observe(rootEl.value); + } + }); +}); + +onBeforeUnmount(() => { + observer.disconnect(); +}); +</script> + +<style lang="scss" module> +.root { + display: block; +} + +.placeholder { + display: block; + min-height: 150px; +} +</style> |