summaryrefslogtreecommitdiff
path: root/packages/frontend/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-03-31 15:01:56 +0900
committersyuilo <Syuilotan@yahoo.co.jp>2023-03-31 15:01:56 +0900
commit9bc5d52e413dcf35e85613e0c5a5b319ba95017a (patch)
treebadac56bd198b281c9e3a4cc662d1533b8076389 /packages/frontend/src
parentrefactor (diff)
downloadmisskey-9bc5d52e413dcf35e85613e0c5a5b319ba95017a.tar.gz
misskey-9bc5d52e413dcf35e85613e0c5a5b319ba95017a.tar.bz2
misskey-9bc5d52e413dcf35e85613e0c5a5b319ba95017a.zip
feat: チャンネルにノートをピン留めできるように
Resolve #7740
Diffstat (limited to 'packages/frontend/src')
-rw-r--r--packages/frontend/src/components/MkNote.vue9
-rw-r--r--packages/frontend/src/pages/channel-editor.vue83
-rw-r--r--packages/frontend/src/pages/channel.vue9
-rw-r--r--packages/frontend/src/pages/clip.vue2
-rw-r--r--packages/frontend/src/scripts/get-note-menu.ts12
5 files changed, 99 insertions, 16 deletions
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 72c6e55df1..eb9793fcc1 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -169,6 +169,7 @@ const props = defineProps<{
}>();
const inChannel = inject('inChannel', null);
+const currentClip = inject<Ref<misskey.entities.Clip> | null>('currentClip', null);
let note = $ref(deepClone(props.note));
@@ -370,8 +371,6 @@ function undoReact(note): void {
});
}
-const currentClipPage = inject<Ref<misskey.entities.Clip> | null>('currentClipPage', null);
-
function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
@@ -386,18 +385,18 @@ function onContextmenu(ev: MouseEvent): void {
ev.preventDefault();
react();
} else {
- os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClipPage }), ev).then(focus);
+ os.contextMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }), ev).then(focus);
}
}
function menu(viaKeyboard = false): void {
- os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClipPage }), menuButton.value, {
+ os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }), menuButton.value, {
viaKeyboard,
}).then(focus);
}
async function clip() {
- os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClipPage }), clipButton.value).then(focus);
+ os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus);
}
function showRenoteMenu(viaKeyboard = false): void {
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue
index 38c5b1e082..667caab966 100644
--- a/packages/frontend/src/pages/channel-editor.vue
+++ b/packages/frontend/src/pages/channel-editor.vue
@@ -2,7 +2,7 @@
<MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
- <div class="_gaps_m">
+ <div v-if="channel" class="_gaps_m">
<MkInput v-model="name">
<template #label>{{ i18n.ts.name }}</template>
</MkInput>
@@ -11,13 +11,37 @@
<template #label>{{ i18n.ts.description }}</template>
</MkTextarea>
- <div class="banner">
+ <div>
<MkButton v-if="bannerId == null" @click="setBannerImage"><i class="ti ti-plus"></i> {{ i18n.ts._channel.setBanner }}</MkButton>
<div v-else-if="bannerUrl">
<img :src="bannerUrl" style="width: 100%;"/>
<MkButton @click="removeBannerImage()"><i class="ti ti-trash"></i> {{ i18n.ts._channel.removeBanner }}</MkButton>
</div>
</div>
+
+ <MkFolder :default-open="true">
+ <template #label>{{ i18n.ts.pinnedNotes }}</template>
+
+ <div class="_gaps">
+ <MkButton primary rounded @click="addPinnedNote()"><i class="ti ti-plus"></i></MkButton>
+
+ <Sortable
+ v-model="pinnedNotes"
+ item-key="id"
+ :handle="'.' + $style.pinnedNoteHandle"
+ :animation="150"
+ >
+ <template #item="{element,index}">
+ <div :class="$style.pinnedNote">
+ <button class="_button" :class="$style.pinnedNoteHandle"><i class="ti ti-menu"></i></button>
+ {{ element.id }}
+ <button class="_button" :class="$style.pinnedNoteRemove" @click="removePinnedNote(index)"><i class="ti ti-x"></i></button>
+ </div>
+ </template>
+ </Sortable>
+ </div>
+ </MkFolder>
+
<div>
<MkButton primary @click="save()"><i class="ti ti-device-floppy"></i> {{ channelId ? i18n.ts.save : i18n.ts.create }}</MkButton>
</div>
@@ -27,7 +51,7 @@
</template>
<script lang="ts" setup>
-import { computed, watch } from 'vue';
+import { computed, ref, watch, defineAsyncComponent } from 'vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
@@ -36,6 +60,9 @@ import * as os from '@/os';
import { useRouter } from '@/router';
import { definePageMetadata } from '@/scripts/page-metadata';
import { i18n } from '@/i18n';
+import MkFolder from '@/components/MkFolder.vue';
+
+const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
const router = useRouter();
@@ -48,6 +75,7 @@ let name = $ref(null);
let description = $ref(null);
let bannerUrl = $ref<string | null>(null);
let bannerId = $ref<string | null>(null);
+const pinnedNotes = ref([]);
watch(() => bannerId, async () => {
if (bannerId == null) {
@@ -70,15 +98,36 @@ async function fetchChannel() {
description = channel.description;
bannerId = channel.bannerId;
bannerUrl = channel.bannerUrl;
+ pinnedNotes.value = channel.pinnedNoteIds.map(id => ({
+ id,
+ }));
}
fetchChannel();
+async function addPinnedNote() {
+ const { canceled, result: value } = await os.inputText({
+ title: i18n.ts.noteIdOrUrl,
+ });
+ if (canceled) return;
+ const note = await os.apiWithDialog('notes/show', {
+ noteId: value.includes('/') ? value.split('/').pop() : value,
+ });
+ pinnedNotes.value = [{
+ id: note.id,
+ }, ...pinnedNotes.value];
+}
+
+function removePinnedNote(index: number) {
+ pinnedNotes.value.splice(index, 1);
+}
+
function save() {
const params = {
name: name,
description: description,
bannerId: bannerId,
+ pinnedNoteIds: pinnedNotes.value.map(x => x.id),
};
if (props.channelId) {
@@ -117,6 +166,32 @@ definePageMetadata(computed(() => props.channelId ? {
}));
</script>
-<style lang="scss" scoped>
+<style lang="scss" module>
+.pinnedNote {
+ position: relative;
+ display: block;
+ line-height: 2.85rem;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ color: var(--navFg);
+}
+
+.pinnedNoteRemove {
+ position: absolute;
+ z-index: 10000;
+ width: 32px;
+ height: 32px;
+ color: #ff2a2a;
+ right: 8px;
+ opacity: 0.8;
+}
+.pinnedNoteHandle {
+ cursor: move;
+ width: 32px;
+ height: 32px;
+ margin: 0 8px;
+ opacity: 0.5;
+}
</style>
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index 0fee2181db..47ca8003ad 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -19,6 +19,13 @@
<MkButton v-if="favorited" v-tooltip="i18n.ts.unfavorite" as-like class="button" rounded primary @click="unfavorite()"><i class="ti ti-star"></i></MkButton>
<MkButton v-else v-tooltip="i18n.ts.favorite" as-like class="button" rounded @click="favorite()"><i class="ti ti-star"></i></MkButton>
+
+ <MkFoldableSection>
+ <template #header><i class="ti ti-pin ti-fw" style="margin-right: 0.5em;"></i>{{ i18n.ts.pinnedNotes }}</template>
+ <div v-if="channel.pinnedNotes.length > 0" class="_gaps">
+ <MkNote v-for="note in channel.pinnedNotes" :key="note.id" class="_panel" :note="note"/>
+ </div>
+ </MkFoldableSection>
</div>
<div v-if="channel && tab === 'timeline'" class="_gaps">
<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
@@ -57,6 +64,8 @@ import MkNotes from '@/components/MkNotes.vue';
import { url } from '@/config';
import MkButton from '@/components/MkButton.vue';
import { defaultStore } from '@/store';
+import MkNote from '@/components/MkNote.vue';
+import MkFoldableSection from '@/components/MkFoldableSection.vue';
const router = useRouter();
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index 2b64de088a..e3ac3f4c9b 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -57,7 +57,7 @@ watch(() => props.clipId, async () => {
immediate: true,
});
-provide('currentClipPage', $$(clip));
+provide('currentClip', $$(clip));
function favorite() {
os.apiWithDialog('clips/favorite', {
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index 00f2523bf9..d91f0b0eb6 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -15,7 +15,7 @@ import { clipsCache } from '@/cache';
export async function getNoteClipMenu(props: {
note: misskey.entities.Note;
isDeleted: Ref<boolean>;
- currentClipPage?: Ref<misskey.entities.Clip>;
+ currentClip?: misskey.entities.Clip;
}) {
const isRenote = (
props.note.renote != null &&
@@ -42,7 +42,7 @@ export async function getNoteClipMenu(props: {
});
if (!confirm.canceled) {
os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id });
- if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true;
+ if (props.currentClip?.id === clip.id) props.isDeleted.value = true;
}
} else {
os.alert({
@@ -92,7 +92,7 @@ export function getNoteMenu(props: {
translation: Ref<any>;
translating: Ref<boolean>;
isDeleted: Ref<boolean>;
- currentClipPage?: Ref<misskey.entities.Clip>;
+ currentClip?: misskey.entities.Clip;
}) {
const isRenote = (
props.note.renote != null &&
@@ -176,7 +176,7 @@ export function getNoteMenu(props: {
}
async function unclip(): Promise<void> {
- os.apiWithDialog('clips/remove-note', { clipId: props.currentClipPage.value.id, noteId: appearNote.id });
+ os.apiWithDialog('clips/remove-note', { clipId: props.currentClip.id, noteId: appearNote.id });
props.isDeleted.value = true;
}
@@ -230,7 +230,7 @@ export function getNoteMenu(props: {
menu = [
...(
- props.currentClipPage?.value.userId === $i.id ? [{
+ props.currentClip?.userId === $i.id ? [{
icon: 'ti ti-backspace',
text: i18n.ts.unclip,
danger: true,
@@ -294,7 +294,7 @@ export function getNoteMenu(props: {
text: i18n.ts.muteThread,
action: () => toggleThreadMute(true),
}),
- appearNote.userId === $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? {
+ appearNote.userId === $i.id ? ($i.pinnedNoteIds ?? []).includes(appearNote.id) ? {
icon: 'ti ti-pinned-off',
text: i18n.ts.unpin,
action: () => togglePin(false),