summaryrefslogtreecommitdiff
path: root/packages/frontend/src/components
diff options
context:
space:
mode:
authordakkar <dakkar@thenautilus.net>2024-11-23 10:41:33 +0000
committerdakkar <dakkar@thenautilus.net>2024-11-23 10:41:33 +0000
commit6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7 (patch)
tree307fcabbac6985abb8d59fa5c16ce021d1c1c7c4 /packages/frontend/src/components
parentfix some lints for frontend (diff)
parentmerge: Move `cypress` to `optionalDependencies` (!697) (diff)
downloadsharkey-6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7.tar.gz
sharkey-6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7.tar.bz2
sharkey-6c13dc04f2d1a7b192d42d5b2a02fddbeb3617c7.zip
Merge branch 'develop' into feature/2024.10
Diffstat (limited to 'packages/frontend/src/components')
-rw-r--r--packages/frontend/src/components/SkFollowingRecentNotes.vue144
-rw-r--r--packages/frontend/src/components/SkRemoteFollowersWarning.vue32
-rw-r--r--packages/frontend/src/components/SkUserRecentNotes.vue2
-rw-r--r--packages/frontend/src/components/global/SkLazy.vue57
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>