Compare commits

...

20 commits

Author SHA1 Message Date
Michcio 96b3e93541 build: Force resolution of types/node 2022-09-14 15:06:35 +02:00
Michcio fe6c1d5e86 backend: Provide type for signedGet 2022-09-14 14:50:55 +02:00
Michcio 27e3225525 backend: Fix type error somewhere 2022-09-14 14:50:55 +02:00
Michcio 0ad239488d backend: Fix logger type to actually accept null 2022-09-14 14:50:55 +02:00
Michcio 53f4b7ad9d Compact mentions to just heads inside notifications 2022-09-14 14:50:53 +02:00
Michcio d4a5e6f147 Preview existing replies in tooltip when hovering reply button 2022-09-14 14:49:11 +02:00
Michcio 8d2aa96c89 Preview replied-to note in tooltip when hovering the arrow 2022-09-14 14:49:09 +02:00
Michcio 8a8f6af36c Exclude CW'd posts from supercompact collapsing 2022-09-14 14:30:38 +02:00
Michcio 147e2bea1c Collapse inline replied-to post to make it more compact 2022-09-14 14:30:37 +02:00
Michcio 53dedb8f0b Shorten notification text by capping it at 75 chars 2022-09-14 14:29:10 +02:00
Michcio 9fe21cd1bc Lazify loading of reactions to users mapping
Borrowed some ideas from code at https://medium.com/js-dojo/lazy-rendering-in-vue-to-improve-performance-dcccd445d5f

Generally the idea is that reaction avatars are now fetched only
when the reaction bar slides into view. This should lower the load
a bit on the server.

TODO: check there might be a glitch when adding a reaction
2022-09-14 14:25:05 +02:00
Michcio 91e1790528 Allow files storage path to be set explicitly 2022-09-14 14:20:42 +02:00
Michcio 3c5891af7b Remove Cypress from dependencies
I don't run e2e tests, and my instance is a Raspberry Pi, and so installing
Cypress is an extra gigabyte of disk space wasted for me.
2022-09-14 14:20:42 +02:00
Michcio 0ff228cace Trim the browser targets list in the transpilation config
I only use Firefox (though I may start using Safari soon), and I do not
particularly care for visitors to my instance, so this is again an attempt
at allowing the compiler to be more reckless wrt polyfilling to minimize
generated code.
2022-09-14 14:20:42 +02:00
Michcio 9e8e06310b Remove Deck UI
I'm not sure this even makes sense, I was just trying to remove more code
from the client.
2022-09-14 14:20:42 +02:00
Michał Sidor 3836b685f6 Modify social timeline to exclude convos with only 1 person I like
This is an attempt at introducing filtering of replies in timeline in the
style of Mastodon or Pleroma's "only replies directed at me or someone I follow".

Currently one way this surely fails is that self-replies by someone I follow in
a conversation solely with someone I don't follow will pass this filter, and
I will see a conversation I don't want to see.

This probably needs more testing to verify that it's doing what's expected of it.
2022-09-14 14:20:42 +02:00
Michał Sidor a0a26835a4 My instance-specific assets and client defaults
I change the favicons and change some of the device-stored
client settings so that I don't have to set them on every
device every time.
2022-09-14 14:20:42 +02:00
Michcio 0cd758117a Add Cherry Bleu theme variant
This is my modification of the dark cherry theme, caused by my
annoyance that you can set a wallpaper in the client, but it's
invisible almost all the time.

What I tried to do here is make a lot more things transparent,
so that the wallpaper would be visible more. It also looks nasty
in some situations, but this is an acceptable tradeoff for me
personally.
2022-09-14 14:20:41 +02:00
Michcio d1a0e522ec Remove all right click context menu functionality
The context menus provided by Misskey, overriding the browser context menus
on right click, were driving me very angry. This makes it much easier to copy
image URLs or even just do a quick "Inspect element".

Side victims: the reaction picker context menu feature. I never used it, so
I am only guessing what it was doing, but since I removed the whole underlying
mechanic, it only felt right to yeet the feature too.
2022-09-14 14:20:36 +02:00
Michał Sidor 5496733a24 Show reacting people next to reaction buttons
This change replaces the reaction count on the reaction buttons under
the post with micro avatars of the people reacting. This makes the
whole thing feel more personal IMHO.

Performance concerns: because the posts by themselves only contain
reaction counts, this means executing an extra API call is done to
fetch the list of users who reacted. This was already being done when
hovering a reaction button, and my Raspberry Pi is doing pretty fine
despite this patch, but it should probably be addressed.
2022-09-14 14:02:02 +02:00
85 changed files with 365 additions and 3147 deletions

View file

@ -686,7 +686,6 @@ apply: "تطبيق"
receiveAnnouncementFromInstance: "استلم إشعارات من هذا المثيل"
emailNotification: "إشعارات البريد الكتروني"
inChannelSearch: "ابحث عن قناة"
useReactionPickerForContextMenu: "افتح منتقي التفاعلات عند النقر بالزر الأيمن"
typingUsers: "{users} يكتب(ون)..."
jumpToSpecifiedDate: "انتقل إلى تاريخ محدد"
showingPastTimeline: "أنت تستعرض حاليًا خيطًا زمنيًا قديمًا"

View file

@ -717,7 +717,6 @@ receiveAnnouncementFromInstance: "এই ইন্সট্যান্স থ
emailNotification: "ইমেইল বিজ্ঞপ্তি"
publish: "প্রকাশ"
inChannelSearch: "চ্যানেলে খুঁজুন"
useReactionPickerForContextMenu: "রাইট ক্লিকের মাধ্যমে রিঅ্যাকশন পিকার খুলুন"
typingUsers: "{users} লেখছে"
jumpToSpecifiedDate: "একটি নির্দিষ্ট তারিখে যান"
showingPastTimeline: "অতীতের টাইমলাইন দেখানো হচ্ছে"

View file

@ -718,7 +718,6 @@ receiveAnnouncementFromInstance: "Benachrichtigungen von dieser Instanz empfange
emailNotification: "Email-Benachrichtigungen"
publish: "Veröffentlichen"
inChannelSearch: "In Kanal suchen"
useReactionPickerForContextMenu: "Reaktionsauswahl durch Rechtsklick öffnen"
typingUsers: "{users} ist/sind am schreiben …"
jumpToSpecifiedDate: "Zu bestimmtem Datum springen"
showingPastTimeline: "Es wird eine alte Chronik angezeigt"

View file

@ -718,7 +718,6 @@ receiveAnnouncementFromInstance: "Receive notifications from this instance"
emailNotification: "Email notifications"
publish: "Publish"
inChannelSearch: "Search in channel"
useReactionPickerForContextMenu: "Open reaction picker on right-click"
typingUsers: "{users} is/are typing..."
jumpToSpecifiedDate: "Jump to specific date"
showingPastTimeline: "Currently displaying an old timeline"

View file

@ -714,7 +714,6 @@ receiveAnnouncementFromInstance: "Recibir notificaciones de la instancia"
emailNotification: "Notificaciones por correo electrónico"
publish: "Publicar"
inChannelSearch: "Buscar en el canal"
useReactionPickerForContextMenu: "Haga clic con el botón derecho para abrir el menu de reacciones"
typingUsers: "{users} está escribiendo"
jumpToSpecifiedDate: "Saltar a una fecha específica"
showingPastTimeline: "Mostrar líneas de tiempo antiguas"

View file

@ -712,7 +712,6 @@ receiveAnnouncementFromInstance: "Recevoir les messages d'information de l'insta
emailNotification: "Notifications par mail"
publish: "Public"
inChannelSearch: "Chercher dans le canal"
useReactionPickerForContextMenu: "Clic-droit pour ouvrir le panneau de réactions"
typingUsers: "{users} est en train d'écrire"
jumpToSpecifiedDate: "Se rendre à la date"
showingPastTimeline: "Un fil ancien est affiché"

View file

@ -717,7 +717,6 @@ receiveAnnouncementFromInstance: "Terima pemberitahuan surel dari instansi ini"
emailNotification: "Pemberitahuan surel"
publish: "Terbitkan"
inChannelSearch: "Cari di kanal"
useReactionPickerForContextMenu: "Buka pemilih reaksi dengan klik-kanan"
typingUsers: "{users} sedang mengetik..."
jumpToSpecifiedDate: "Loncat ke tanggal spesifik"
showingPastTimeline: "Sedang menampilkan linimasa lama"

View file

@ -706,7 +706,6 @@ receiveAnnouncementFromInstance: "Ricevi i messaggi informativi dall'istanza"
emailNotification: "Eventi per notifiche via mail"
publish: "Pubblico"
inChannelSearch: "Cerca in canale"
useReactionPickerForContextMenu: "Cliccare sul tasto destro per aprire il pannello di reazioni"
typingUsers: "{users} sta(nno) scrivendo"
jumpToSpecifiedDate: "Vai alla data "
showingPastTimeline: "Stai visualizzando una vecchia timeline"

View file

@ -720,7 +720,6 @@ receiveAnnouncementFromInstance: "インスタンスからのお知らせを受
emailNotification: "メール通知"
publish: "公開"
inChannelSearch: "チャンネル内検索"
useReactionPickerForContextMenu: "右クリックでリアクションピッカーを開く"
typingUsers: "{users}が入力中"
jumpToSpecifiedDate: "特定の日付にジャンプ"
showingPastTimeline: "過去のタイムラインを表示しています"

View file

@ -629,7 +629,6 @@ apply: "適用"
receiveAnnouncementFromInstance: "インスタンスからのお知らせを受け取る"
emailNotification: "メール通知"
inChannelSearch: "チャンネル内検索"
useReactionPickerForContextMenu: "右クリックでリアクションピッカーを開くようにする"
typingUsers: "{users}が今書きよるで"
jumpToSpecifiedDate: "特定の日付にジャンプ"
showingPastTimeline: "過去のタイムラインを表示してるで"

View file

@ -717,7 +717,6 @@ receiveAnnouncementFromInstance: "이 인스턴스의 알림을 이메일로 수
emailNotification: "메일 알림"
publish: "게시"
inChannelSearch: "채널에서 검색"
useReactionPickerForContextMenu: "우클릭하여 리액션 선택기 열기"
typingUsers: "{users} 님이 입력하고 있어요.."
jumpToSpecifiedDate: "특정 날짜로 이동"
showingPastTimeline: "과거의 타임라인을 표시하고 있어요"

View file

@ -693,7 +693,6 @@ receiveAnnouncementFromInstance: "Otrzymuj powiadomienia e-mail z tej instancji"
emailNotification: "Powiadomienia e-mail"
publish: "Publikuj"
inChannelSearch: "Szukaj na kanale"
useReactionPickerForContextMenu: "Otwórz wybornik reakcji prawym kliknięciem"
typingUsers: "{users} pisze(-ą)..."
jumpToSpecifiedDate: "Przejdź do określonej daty"
showingPastTimeline: "Obecnie wyświetla starą oś czasu"

View file

@ -715,7 +715,6 @@ receiveAnnouncementFromInstance: "Получать оповещения с ин
emailNotification: "Уведомления по электронной почте"
publish: "Опубликовать"
inChannelSearch: "Поиск по каналу"
useReactionPickerForContextMenu: "Открывать палитру реакций правой кнопкой"
typingUsers: "Стук клавиш. Это {users}…"
jumpToSpecifiedDate: "Перейти к заданной дате"
showingPastTimeline: "Отображается старая лента"

View file

@ -716,7 +716,6 @@ receiveAnnouncementFromInstance: "Prijať notifikácie z tohoto servera"
emailNotification: "Emailové upozornenia"
publish: "Zverejniť"
inChannelSearch: "Hľadať v kanáli"
useReactionPickerForContextMenu: "Otvoriť výber reakcií na pravý klik"
typingUsers: "{users} píše/u"
jumpToSpecifiedDate: "Skočiť na konkrétny dátum"
showingPastTimeline: "Práve vidíte starú časovú os"

View file

@ -717,7 +717,6 @@ receiveAnnouncementFromInstance: "Отримувати оповіщення з
emailNotification: "Сповіщення електронною поштою"
publish: "Опублікувати"
inChannelSearch: "Пошук за каналом"
useReactionPickerForContextMenu: "Відкривати палітру реакцій правою кнопкою"
typingUsers: "Стук клавіш. Це {users}…"
goBack: "Назад"
info: "Інформація"

View file

@ -717,7 +717,6 @@ receiveAnnouncementFromInstance: "Nhận thông báo từ máy chủ này"
emailNotification: "Thông báo email"
publish: "Đăng"
inChannelSearch: "Tìm trong kênh"
useReactionPickerForContextMenu: "Nhấn chuột phải để mở bộ chọn biểu cảm"
typingUsers: "{users} đang nhập…"
jumpToSpecifiedDate: "Đến một ngày cụ thể"
showingPastTimeline: "Hiện đang hiển thị dòng thời gian cũ"

View file

@ -717,7 +717,6 @@ receiveAnnouncementFromInstance: "从实例接收通知"
emailNotification: "邮件通知"
publish: "发布"
inChannelSearch: "频道内搜索"
useReactionPickerForContextMenu: "单击右键打开回应工具栏"
typingUsers: "{users}正在输入"
jumpToSpecifiedDate: "跳转到特定日期"
showingPastTimeline: "显示过去的时间线"

View file

@ -717,7 +717,6 @@ receiveAnnouncementFromInstance: "接收由本實例發出的電郵通知"
emailNotification: "郵件通知"
publish: "發佈"
inChannelSearch: "頻道内搜尋"
useReactionPickerForContextMenu: "點擊右鍵開啟回應工具欄"
typingUsers: "{users}輸入中..."
jumpToSpecifiedDate: "跳轉到特定日期"
showingPastTimeline: "顯示過往的時間線"

View file

@ -32,7 +32,8 @@
},
"resolutions": {
"chokidar": "^3.3.1",
"lodash": "^4.17.21"
"lodash": "^4.17.21",
"@types/node": "^18.7.18"
},
"dependencies": {
"execa": "5.1.1",
@ -48,7 +49,6 @@
"@types/gulp-rename": "2.0.1",
"@typescript-eslint/parser": "^5.36.2",
"cross-env": "7.0.3",
"cypress": "10.3.0",
"start-server-and-test": "1.14.0",
"typescript": "4.8.3"
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 118 KiB

View file

@ -63,6 +63,7 @@ export type Source = {
mediaProxy?: string;
proxyRemoteFiles?: boolean;
internalStoragePath?: string;
};
/**

View file

@ -34,7 +34,7 @@ export default async (user: { id: User['id'] }, url: string, object: any) => {
* @param user http-signature user
* @param url URL to fetch
*/
export async function signedGet(url: string, user: { id: User['id'] }) {
export async function signedGet(url: string, user: { id: User['id'] }): Promise<any> {
const keypair = await getUserKeypair(user.id);
const req = createSignedGet({

View file

@ -1,7 +1,7 @@
import { Brackets, SelectQueryBuilder } from 'typeorm';
import { User } from '@/models/entities/user.js';
export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User, 'id' | 'showTimelineReplies'> | null) {
export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User, 'id' | 'showTimelineReplies'> | null, followingQuery: SelectQueryBuilder<any> | null) {
if (me == null) {
q.andWhere(new Brackets(qb => { qb
.where('note.replyId IS NULL') // 返信ではない
@ -14,6 +14,7 @@ export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User,
q.andWhere(new Brackets(qb => { qb
.where('note.replyId IS NULL') // 返信ではない
.orWhere('note.replyUserId = :meId', { meId: me.id }) // 返信だけど自分のノートへの返信
.orWhere('note.mentions && array[:meId]::varchar[]', { meId: me.id })
.orWhere(new Brackets(qb => { qb // 返信だけど自分の行った返信
.where('note.replyId IS NOT NULL')
.andWhere('note.userId = :meId', { meId: me.id });
@ -22,6 +23,12 @@ export function generateRepliesQuery(q: SelectQueryBuilder<any>, me?: Pick<User,
.where('note.replyId IS NOT NULL')
.andWhere('note.replyUserId = note.userId');
}));
if (followingQuery !== null) {
qb.orWhere(new Brackets(qb => { qb
.where(`note.mentions && array(${ followingQuery.getQuery() })`)
.setParameters(followingQuery.getParameters())
}))
}
}));
}
}

View file

@ -74,7 +74,7 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect('renoteUser.avatar', 'renoteUserAvatar')
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
generateRepliesQuery(query, user);
generateRepliesQuery(query, user, null);
if (user) {
generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);

View file

@ -88,7 +88,7 @@ export default define(meta, paramDef, async (ps, user) => {
.setParameters(followingQuery.getParameters());
generateChannelQuery(query, user);
generateRepliesQuery(query, user);
generateRepliesQuery(query, user, followingQuery);
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);

View file

@ -81,7 +81,7 @@ export default define(meta, paramDef, async (ps, user) => {
.leftJoinAndSelect('renoteUser.banner', 'renoteUserBanner');
generateChannelQuery(query, user);
generateRepliesQuery(query, user);
generateRepliesQuery(query, user, null);
generateVisibilityQuery(query, user);
if (user) generateMutedUserQuery(query, user);
if (user) generateMutedNoteQuery(query, user);

View file

@ -80,7 +80,7 @@ export default define(meta, paramDef, async (ps, user) => {
.setParameters(followingQuery.getParameters());
generateChannelQuery(query, user);
generateRepliesQuery(query, user);
generateRepliesQuery(query, user, followingQuery);
generateVisibilityQuery(query, user);
generateMutedUserQuery(query, user);
generateMutedNoteQuery(query, user);

View file

@ -38,11 +38,8 @@ export default class extends Channel {
// Ignore notes from instances the user has muted
if (isInstanceMuted(note, new Set<string>(this.userProfile?.mutedInstances ?? []))) return;
// 関係ない返信は除外
if (note.reply && !this.user!.showTimelineReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user!.id && note.userId !== this.user!.id && reply.userId !== note.userId) return;
if (note.reply && note.mentions && !this.user!.showTimelineReplies) {
if (!note.mentions.includes(this.user!.id) && !note.mentions.some((user: string) => this.following.has(user))) return;
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する

View file

@ -138,7 +138,7 @@ export const startServer = () => {
return server;
};
export default () => new Promise(resolve => {
export default () => new Promise<void>(resolve => {
const server = createServer();
initializeStreamingServer(server);

View file

@ -8,7 +8,7 @@ const _filename = fileURLToPath(import.meta.url);
const _dirname = dirname(_filename);
export class InternalStorage {
private static readonly path = Path.resolve(_dirname, '../../../../../files');
private static readonly path = config.internalStoragePath || Path.resolve(_dirname, '../../../../../files');
public static resolvePath = (key: string) => Path.resolve(InternalStorage.path, key);

View file

@ -94,7 +94,10 @@ export default class Logger {
}
}
public error(x: string | Error, data?: Record<string, any> = {}, important = false): void { // 実行を継続できない状況で使う
public error(x: string | Error, data: Record<string, any> | null = {}, important = false): void { // 実行を継続できない状況で使う
if (data === null) {
data = {};
}
if (x instanceof Error) {
data.e = x;
this.log('error', x.toString(), data, important);

View file

@ -18,6 +18,7 @@
"@syuilo/aiscript": "0.11.1",
"@vitejs/plugin-vue": "^3.1.0",
"@vue/compiler-sfc": "3.2.39",
"@vueuse/core": "9.1.0",
"abort-controller": "3.0.0",
"autobind-decorator": "2.4.0",
"autosize": "5.0.1",
@ -100,7 +101,6 @@
"@typescript-eslint/eslint-plugin": "^5.36.2",
"@typescript-eslint/parser": "^5.36.2",
"cross-env": "7.0.3",
"cypress": "10.3.0",
"eslint": "^8.20.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-vue": "^9.1.1",

View file

@ -1,16 +1,16 @@
<template>
<div v-size="{ max: [450] }" class="wrpstxzv" :class="{ children: depth > 1 }">
<div v-size="{ max: [450] }" class="wrpstxzv" :class="{ children: depth > 1, supercompact }">
<div class="main">
<MkAvatar class="avatar" :user="note.user"/>
<div class="body">
<XNoteHeader class="header" :note="note" :mini="true"/>
<XNoteHeader :supercompact="supercompact" class="header" :note="note" :mini="true"/>
<div class="body">
<p v-if="note.cw != null" class="cw">
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
<XCwButton v-model="showContent" :note="note"/>
</p>
<div v-show="note.cw == null || showContent" class="content">
<MkNoteSubNoteContent class="text" :note="note"/>
<MkNoteSubNoteContent class="text" :note="note" :supercompact="supercompact && note.cw == null" :compact-heads="compactHeads"/>
</div>
</div>
</div>
@ -38,11 +38,15 @@ const props = withDefaults(defineProps<{
note: foundkey.entities.Note;
conversation?: foundkey.entities.Note[] | null;
supercompact?: boolean;
compactHeads?: boolean;
// how many notes are in between this one and the note being viewed in detail
depth?: number;
}>(), {
conversation: null,
depth: 1,
supercompact: false,
compactHeads: false,
});
let showContent = $ref(false);
@ -67,6 +71,13 @@ const replies: foundkey.entities.Note[] = props.conversation?.filter(item => ite
}
}
&.supercompact {
> .main > .avatar {
width: 30px;
height: 30px;
}
}
> .main {
display: flex;
@ -76,7 +87,7 @@ const replies: foundkey.entities.Note[] = props.conversation?.filter(item => ite
margin: 0 8px 0 0;
width: 38px;
height: 38px;
border-radius: 8px;
border-radius: calc(8% / 38);
}
> .body {

View file

@ -0,0 +1,28 @@
<template>
<MkTooltip :showing="true" :target-element="targetElement" :max-width="400" @closed="emit('closed')">
<div class="tooltip-note">
<MkNoteSub v-for="note in notes" :key="note.id" :note="note"/>
</div>
</MkTooltip>
</template>
<script lang="ts" setup>
import * as misskey from 'foundkey-js';
import MkTooltip from './ui/tooltip.vue';
import MkNoteSub from './MkNoteSub.vue';
defineProps<{
notes: misskey.entities.Note[];
targetElement: HTMLElement;
}>();
const emit = defineEmits<{
(ev: 'closed'): void;
}>();
</script>
<style lang="scss" scoped>
.tooltip-note {
text-align: left;
}
</style>

View file

@ -1,5 +1,5 @@
<template>
<div ref="rootEl" class="swhvrteh _popup _shadow" :style="{ zIndex }" @contextmenu.prevent="() => {}">
<div ref="rootEl" class="swhvrteh _popup _shadow" :style="{ zIndex }">
<ol v-if="type === 'user'" ref="suggests" class="users">
<li v-for="user in users" tabindex="-1" class="user" @click="complete(type, user)" @keydown="onKeydown">
<img class="avatar" :src="user.avatarUrl"/>

View file

@ -5,7 +5,6 @@
draggable="true"
:title="title"
@click="onClick"
@contextmenu.stop="onContextmenu"
@dragstart="onDragstart"
@dragend="onDragend"
>
@ -101,10 +100,6 @@ function onClick(ev: MouseEvent): void {
}
}
function onContextmenu(ev: MouseEvent): void {
os.contextMenu(getMenu(), ev);
}
function onDragstart(ev: DragEvent): void {
if (ev.dataTransfer) {
ev.dataTransfer.effectAllowed = 'move';

View file

@ -5,7 +5,6 @@
draggable="true"
:title="title"
@click="onClick"
@contextmenu.stop="onContextmenu"
@mouseover="onMouseover"
@mouseout="onMouseout"
@dragover.prevent.stop="onDragover"
@ -218,27 +217,6 @@ function deleteFolder() {
});
}
function onContextmenu(ev: MouseEvent) {
os.contextMenu([{
text: i18n.ts.openInWindow,
icon: 'fas fa-window-restore',
action: () => {
os.popup(defineAsyncComponent(() => import('./drive-window.vue')), {
initialFolder: props.folder,
}, {
}, 'closed');
},
}, null, {
text: i18n.ts.rename,
icon: 'fas fa-i-cursor',
action: rename,
}, null, {
text: i18n.ts.delete,
icon: 'fas fa-trash-alt',
danger: true,
action: deleteFolder,
}], ev);
}
</script>
<style lang="scss" scoped>

View file

@ -1,7 +1,7 @@
<template>
<div class="yfudmmck">
<nav>
<div class="path" @contextmenu.prevent.stop="() => {}">
<div class="path">
<XNavFolder
:class="{ current: folder == null }"
:parent-folder="folder"
@ -33,7 +33,6 @@
@dragenter="onDragenter"
@dragleave="onDragleave"
@drop.prevent.stop="onDrop"
@contextmenu.stop="onContextmenu"
>
<div ref="contents" class="contents">
<div v-show="folders.length > 0" ref="foldersContainer" class="folders">
@ -601,10 +600,6 @@ function showMenu(ev: MouseEvent) {
os.popupMenu(getMenu(), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
}
function onContextmenu(ev: MouseEvent) {
os.contextMenu(getMenu(), ev);
}
onMounted(() => {
if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) {
nextTick(() => {

View file

@ -1,5 +1,5 @@
<template>
<a :href="to" :class="active ? activeClass : null" @click.prevent="nav" @contextmenu.prevent.stop="onContextmenu">
<a :href="to" :class="active ? activeClass : null" @click.prevent="nav">
<slot></slot>
</a>
</template>
@ -32,39 +32,6 @@ const active = $computed(() => {
return resolved.route.name === router.currentRoute.value.name;
});
function onContextmenu(ev) {
const selection = window.getSelection();
if (selection && selection.toString() !== '') return;
os.contextMenu([{
type: 'label',
text: props.to,
}, {
icon: 'fas fa-window-maximize',
text: i18n.ts.openInWindow,
action: () => {
os.pageWindow(props.to);
},
}, {
icon: 'fas fa-expand-alt',
text: i18n.ts.showInPage,
action: () => {
router.push(props.to);
},
}, null, {
icon: 'fas fa-external-link-alt',
text: i18n.ts.openInNewTab,
action: () => {
window.open(props.to, '_blank');
},
}, {
icon: 'fas fa-link',
text: i18n.ts.copyLink,
action: () => {
copyToClipboard(`${url}${props.to}`);
},
}], ev);
}
function nav() {
if (props.behavior === 'browser') {
location.href = props.to;

View file

@ -1,5 +1,5 @@
<template>
<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :custom-emojis="customEmojis" :is-note="isNote" class="havbbuyv" :class="{ nowrap }"/>
<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :custom-emojis="customEmojis" :is-note="isNote" class="havbbuyv" :class="{ nowrap }" :compact-heads="compactHeads"/>
</template>
<script lang="ts" setup>
@ -12,6 +12,7 @@ withDefaults(defineProps<{
author?: any;
customEmojis?: any;
isNote?: boolean;
compactHeads?: boolean;
}>(), {
plain: false,
nowrap: false,

View file

@ -1,7 +1,6 @@
<template>
<component
:is="self ? 'MkA' : 'a'" ref="el" class="ieqqeuvs _link" :[attr]="self ? url.slice(local.length) : url" :rel="rel" :target="target"
@contextmenu.stop="() => {}"
>
<template v-if="!self">
<span class="schema">{{ schema }}//</span>

View file

@ -12,7 +12,6 @@
:alt="video.comment"
preload="none"
controls
@contextmenu.stop
>
<source
:src="video.url"

View file

@ -1,7 +1,7 @@
<template>
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }">
<img :class="$style.icon" :src="`/avatar/@${username}@${host}`" alt="">
<span class="main">
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe, [$style.compact]: compact }]" :to="url" :style="{ background: bgCss }">
<img :class="[$style.icon, { [$style.compact]: compact }]" :src="`/avatar/@${username}@${host}`" alt="">
<span v-if="!compact" class="main">
<span class="username">@{{ username }}</span>
<span v-if="(host != localHost) || defaultStore.state.showFullAcct" :class="$style.mainHost">@{{ toUnicode(host) }}</span>
</span>
@ -25,6 +25,7 @@ import { defaultStore } from '@/store';
const props = defineProps<{
username: string;
host: string;
compact?: boolean;
}>();
const canonical = props.host === localHost ? `@${props.username}` : `@${props.username}@${toUnicode(props.host)}`;
@ -49,6 +50,10 @@ useCssModule();
border-radius: 999px;
color: var(--mention);
&.compact {
padding: 0;
}
&.isMe {
color: var(--mentionMe);
}
@ -61,6 +66,14 @@ useCssModule();
margin: 0 0.2em 0 0;
vertical-align: bottom;
border-radius: 100%;
.compact > & {
margin: 0;
border: 0.1em solid var(--mention);
}
.compact.isMe > & {
border: 0.1em solid var(--mentionMe);
}
}
.mainHost {

View file

@ -42,6 +42,11 @@ export default defineComponent({
type: Boolean,
default: true,
},
compactHeads: {
required: false,
type: Boolean,
default: false,
}
},
render() {
@ -232,6 +237,7 @@ export default defineComponent({
key: Math.random(),
host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host,
username: token.props.username,
compact: this.compactHeads,
})];
}

View file

@ -1,7 +1,7 @@
<template>
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
<div ref="rootEl" class="hrmcaedk _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
<div class="header" @contextmenu="onContextmenu">
<div class="header">
<button v-if="history.length > 0" v-tooltip="$ts.goBack" class="_button" @click="back()"><i class="fas fa-arrow-left"></i></button>
<span v-else style="display: inline-block; width: 20px"></span>
<span v-if="pageMetadata?.value" class="title">
@ -59,33 +59,6 @@ provide('shouldOmitHeaderTitle', true);
provide('shouldHeaderThin', true);
const pageUrl = $computed(() => url + path);
const contextmenu = $computed(() => {
return [{
type: 'label',
text: path,
}, {
icon: 'fas fa-expand-alt',
text: i18n.ts.showInPage,
action: expand,
}, {
icon: 'fas fa-external-link-alt',
text: i18n.ts.popout,
action: popout,
}, null, {
icon: 'fas fa-external-link-alt',
text: i18n.ts.openInNewTab,
action: () => {
window.open(pageUrl, '_blank');
modal.close();
},
}, {
icon: 'fas fa-link',
text: i18n.ts.copyLink,
action: () => {
copyToClipboard(pageUrl);
},
}];
});
function navigate(path, record = true) {
if (record) history.push(router.getCurrentPath());
@ -105,10 +78,6 @@ function popout() {
_popout(path, rootEl);
modal.close();
}
function onContextmenu(ev: MouseEvent) {
os.contextMenu(contextmenu, ev);
}
</script>
<style lang="scss" scoped>

View file

@ -29,7 +29,7 @@
<MkVisibility :note="note"/>
</div>
</div>
<article class="article" @contextmenu.stop="onContextmenu">
<article class="article">
<header class="header">
<MkAvatar class="avatar" :user="appearNote.user" :show-indicator="true"/>
<div class="body">
@ -230,24 +230,6 @@ function undoReact(note): void {
});
}
function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(ev.target)) return;
if (window.getSelection().toString() !== '') return;
if (defaultStore.state.useReactionPickerForContextMenu) {
ev.preventDefault();
react();
} else {
os.contextMenu(getNoteMenu({ note, translating, translation, menuButton, isDeleted }), ev).then(focus);
}
}
function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note, translating, translation, menuButton, isDeleted }), menuButton.value, {
viaKeyboard,

View file

@ -1,10 +1,11 @@
<template>
<header class="kkwtjztg">
<MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)">
<MkUserName :user="note.user"/>
<MkUserName v-if="!supercompact" :user="note.user"/>
<div v-if="supercompact" class="username"><MkAcct :user="note.user"/></div>
</MkA>
<div v-if="note.user.isBot" class="is-bot">bot</div>
<div class="username"><MkAcct :user="note.user"/></div>
<div v-if="!supercompact" class="username"><MkAcct :user="note.user"/></div>
<div class="info">
<MkA class="created-at" :to="notePage(note)">
<MkTime :time="note.createdAt"/>
@ -23,6 +24,7 @@ import { userPage } from '@/filters/user';
defineProps<{
note: foundkey.entities.Note;
pinned?: boolean;
supercompact?: boolean;
}>();
</script>

View file

@ -9,7 +9,7 @@
:tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }"
>
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :supercompact="compactParent" :compact-heads="compactHeads" class="reply-to"/>
<div v-if="pinned" class="info"><i class="fas fa-thumbtack"></i> {{ i18n.ts.pinnedNote }}</div>
<div v-if="appearNote._featuredId_" class="info"><i class="fas fa-bolt"></i> {{ i18n.ts.featured }}</div>
<div v-if="isRenote" class="renote">
@ -30,7 +30,7 @@
<MkVisibility :note="note"/>
</div>
</div>
<article class="article" @contextmenu.stop="onContextmenu">
<article class="article">
<MkAvatar class="avatar" :user="appearNote.user"/>
<div class="main">
<XNoteHeader class="header" :note="appearNote" :mini="true"/>
@ -42,8 +42,8 @@
</p>
<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed, isLong }">
<div class="text">
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="fas fa-reply"></i></MkA>
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i ref="parentReply" class="fas fa-reply"></i></MkA>
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis" :compact-heads="compactHeads"/>
<a v-if="appearNote.renote != null" class="rp">RN:</a>
<div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini/>
@ -70,7 +70,7 @@
</div>
<footer class="footer">
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
<button class="button _button" @click="reply()">
<button ref="replyButton" class="button _button" @click="reply()">
<template v-if="appearNote.reply"><i class="fas fa-reply-all"></i></template>
<template v-else><i class="fas fa-reply"></i></template>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
@ -107,6 +107,7 @@ import * as foundkey from 'foundkey-js';
import MkNoteSub from './MkNoteSub.vue';
import XNoteHeader from './note-header.vue';
import XNoteSimple from './note-simple.vue';
import NoteTooltip from './NoteTooltip.vue';
import XReactionsViewer from './reactions-viewer.vue';
import XMediaList from './media-list.vue';
import XCwButton from './cw-button.vue';
@ -127,10 +128,13 @@ import { $i } from '@/account';
import { i18n } from '@/i18n';
import { getNoteMenu } from '@/scripts/get-note-menu';
import { useNoteCapture } from '@/scripts/use-note-capture';
import { useTooltip } from '@/scripts/use-tooltip';
const props = defineProps<{
note: foundkey.entities.Note;
pinned?: boolean;
compactParent?: boolean;
compactHeads?: boolean;
}>();
const inChannel = inject('inChannel', null);
@ -157,9 +161,11 @@ const isRenote = (
const el = ref<HTMLElement>();
const menuButton = ref<HTMLElement>();
const replyButton = ref<HTMLElement>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
const renoteTime = ref<HTMLElement>();
const reactButton = ref<HTMLElement>();
const parentReply = ref<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as foundkey.entities.Note : note);
const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false);
@ -192,6 +198,30 @@ useNoteCapture({
isDeletedRef: isDeleted,
});
useTooltip(parentReply, async (showing) => {
const parentNote = await os.api('notes/show', {
noteId: appearNote.replyId,
});
os.popup(NoteTooltip, {
showing,
notes: [parentNote],
targetElement: parentReply.value,
}, {}, 'closed');
});
useTooltip(replyButton, async (showing) => {
const replies = await os.api('notes/replies', {
noteId: appearNote.id,
});
os.popup(NoteTooltip, {
showing,
notes: replies,
targetElement: replyButton.value,
}, {}, 'closed');
});
function reply(viaKeyboard = false): void {
pleaseLogin();
os.post({
@ -224,24 +254,6 @@ function undoReact(): void {
const currentClipPage = inject<Ref<foundkey.entities.Clip> | null>('currentClipPage', null);
function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(ev.target)) return;
if (window.getSelection().toString() !== '') return;
if (defaultStore.state.useReactionPickerForContextMenu) {
ev.preventDefault();
react();
} else {
os.contextMenu(getNoteMenu({ note, translating, translation, menuButton, isDeleted, currentClipPage }), ev).then(focus);
}
}
function menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note, translating, translation, menuButton, isDeleted, currentClipPage }), menuButton.value, {
viaKeyboard,

View file

@ -10,7 +10,7 @@
<template #default="{ items: notes }">
<div class="giivymft" :class="{ noGap }">
<XList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" class="notes">
<XNote :key="note._featuredId_ || note.id" class="qtqtichx" :note="note"/>
<XNote :key="note._featuredId_ || note.id" class="qtqtichx" :note="note" :compact-parent="true"/>
</XList>
</div>
</template>

View file

@ -9,7 +9,7 @@
<template #default="{ items: notifications }">
<XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true">
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
<XNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :compact-parent="true" :compact-heads="true" />
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
</XList>
</template>

View file

@ -7,7 +7,6 @@
:close-button="true"
:buttons-left="buttonsLeft"
:buttons-right="buttonsRight"
:contextmenu="contextmenu"
@closed="$emit('closed')"
>
<template #header>
@ -85,28 +84,6 @@ provideMetadataReceiver((info) => {
provide('shouldOmitHeaderTitle', true);
provide('shouldHeaderThin', true);
const contextmenu = $computed(() => ([{
icon: 'fas fa-expand-alt',
text: i18n.ts.showInPage,
action: expand,
}, {
icon: 'fas fa-external-link-alt',
text: i18n.ts.popout,
action: popout,
}, {
icon: 'fas fa-external-link-alt',
text: i18n.ts.openInNewTab,
action: () => {
window.open(url + router.getCurrentPath(), '_blank');
windowEl.close();
},
}, {
icon: 'fas fa-link',
text: i18n.ts.copyLink,
action: () => {
copyToClipboard(url + router.getCurrentPath());
},
}]));
function back() {
history.pop();

View file

@ -2,7 +2,7 @@
<div v-show="files.length != 0" class="skeikyzd">
<XDraggable v-model="_files" class="files" item-key="id" animation="150" delay="100" delay-on-touch-only="true">
<template #item="{element}">
<div @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
<div @click="showFileMenu(element, $event)">
<MkDriveFileThumbnail :data-id="element.id" class="thumbnail" :file="element" fit="cover"/>
<div v-if="element.isSensitive" class="sensitive">
<i class="fas fa-exclamation-triangle icon"></i>

View file

@ -8,7 +8,10 @@
@click="toggleReaction()"
>
<MkEmoji class="icon" :emoji="reaction" :custom-emojis="note.emojis" :is-reaction="true" :normal="true"/>
<span class="count">{{ count }}</span>
<span v-if="users === undefined" class="count">{{ count }}</span>
<span v-if="users !== undefined">
<MkAvatar v-for="u in users" class="user" :style="{ height: '2em', width: '2em' }" :key="u.id" :user="u" />
</span>
</button>
</template>
@ -25,13 +28,14 @@ const props = defineProps<{
count: number;
isInitial: boolean;
note: foundkey.entities.Note;
users?: foundkey.entities.UserLite[];
}>();
const buttonRef = ref<HTMLElement>();
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
const toggleReaction = () => {
const toggleReaction = (): void => {
if (!canToggle.value) return;
const oldReaction = props.note.myReaction;

View file

@ -1,14 +1,16 @@
<template>
<div class="tdflqwzn" :class="{ isMe }">
<XReaction v-for="(count, reaction) in note.reactions" :key="reaction" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :note="note"/>
<div ref="targetEl" class="tdflqwzn" :class="{ isMe }">
<XReaction v-for="(count, reaction) in note.reactions" :key="reaction" :reaction="reaction" :count="count" :is-initial="initialReactions.has(reaction)" :users="usersMap[reaction]" :note="note"/>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { computed, onMounted, ref, watch } from 'vue';
import { useIntersectionObserver } from '@vueuse/core';
import * as foundkey from 'foundkey-js';
import XReaction from './reactions-viewer.reaction.vue';
import { $i } from '@/account';
import * as os from '@/os';
const props = defineProps<{
note: foundkey.entities.Note;
@ -17,6 +19,39 @@ const props = defineProps<{
const initialReactions = new Set(Object.keys(props.note.reactions));
const isMe = computed(() => $i && $i.id === props.note.userId);
const usersMap = ref({});
const superloaded = ref(false);
const targetEl = ref();
async function updateReactions(currentValue): Promise<void> {
const reactions = await os.api('notes/reactions', {
noteId: currentValue.id,
limit: 100,
});
const users = {};
for (const reaction of reactions) {
if (users[reaction.type] === undefined) {
users[reaction.type] = [];
}
users[reaction.type].push(reaction.user);
}
usersMap.value = users;
superloaded.value = true;
}
async function prepareFetch(note: foundkey.entities.Note): Promise<void> {
superloaded.value = false;
const { stop } = useIntersectionObserver(targetEl, ([{ isIntersecting }]) => {
if (isIntersecting) {
superloaded.value = true;
stop();
updateReactions(note);
}
});
}
onMounted(() => prepareFetch(props.note));
watch(props.note, prepareFetch, { deep: true });
</script>
<style lang="scss" scoped>

View file

@ -1,9 +1,9 @@
<template>
<div class="wrmlmaau" :class="{ collapsed, isLong }">
<div class="wrmlmaau" :class="{ collapsed, isLong, supercompact }">
<div class="body">
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="fas fa-reply"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i ref="parentReply" class="fas fa-reply"></i></MkA>
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis" :compact-heads="compactHeads"/>
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
</div>
<details v-if="note.files.length > 0">
@ -15,7 +15,7 @@
<XPoll :note="note"/>
</details>
<button v-if="isLong && collapsed" class="fade _button" @click="collapsed = false">
<span>{{ i18n.ts.showMore }}</span>
<span v-if="!supercompact">{{ i18n.ts.showMore }}</span>
</button>
<button v-if="isLong && !collapsed" class="showLess _button" @click="collapsed = true">
<span>{{ i18n.ts.showLess }}</span>
@ -24,13 +24,19 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import * as foundkey from 'foundkey-js';
import XPoll from './poll.vue';
import XMediaList from './media-list.vue';
import NoteTooltip from './NoteTooltip.vue';
import { i18n } from '@/i18n';
import * as os from '@/os';
import { useTooltip } from '@/scripts/use-tooltip';
const props = defineProps<{
note: foundkey.entities.Note;
supercompact?: boolean;
compactHeads?: boolean;
}>();
const isLong = (
@ -39,7 +45,20 @@ const isLong = (
(props.note.text.length > 500)
)
);
const collapsed = $ref(props.note.cw == null && isLong);
const collapsed = $ref(props.supercompact || (props.note.cw == null && isLong));
const parentReply = ref<HTMLElement>();
useTooltip(parentReply, async (showing): Promise<void> => {
const parentNote = await os.api('notes/show', {
noteId: props.note.replyId,
});
os.popup(NoteTooltip, {
showing,
notes: [parentNote],
targetElement: parentReply.value,
}, {}, 'closed');
});
</script>
<style lang="scss" scoped>
@ -64,6 +83,10 @@ const collapsed = $ref(props.note.cw == null && isLong);
max-height: 9em;
overflow: hidden;
&.supercompact {
max-height: 4.5em;
}
> .fade {
display: block;
position: absolute;

View file

@ -1,85 +0,0 @@
<template>
<transition :name="$store.state.animation ? 'fade' : ''" appear>
<div ref="rootEl" class="nvlagfpb" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
<MkMenu :items="items" class="_popup _shadow" :align="'left'" @close="$emit('closed')"/>
</div>
</transition>
</template>
<script lang="ts" setup>
import { onMounted, onBeforeUnmount } from 'vue';
import MkMenu from './menu.vue';
import { MenuItem } from './types/menu.vue';
import contains from '@/scripts/contains';
import * as os from '@/os';
const props = defineProps<{
items: MenuItem[];
ev: MouseEvent;
}>();
const emit = defineEmits<{
(ev: 'closed'): void;
}>();
let rootEl = $ref<HTMLDivElement>();
let zIndex = $ref<number>(os.claimZIndex('high'));
onMounted(() => {
let left = props.ev.pageX + 1; // + 1
let top = props.ev.pageY + 1; // + 1
const width = rootEl.offsetWidth;
const height = rootEl.offsetHeight;
if (left + width - window.pageXOffset > window.innerWidth) {
left = window.innerWidth - width + window.pageXOffset;
}
if (top + height - window.pageYOffset > window.innerHeight) {
top = window.innerHeight - height + window.pageYOffset;
}
if (top < 0) {
top = 0;
}
if (left < 0) {
left = 0;
}
rootEl.style.top = `${top}px`;
rootEl.style.left = `${left}px`;
for (const el of Array.from(document.querySelectorAll('body *'))) {
el.addEventListener('mousedown', onMousedown);
}
});
onBeforeUnmount(() => {
for (const el of Array.from(document.querySelectorAll('body *'))) {
el.removeEventListener('mousedown', onMousedown);
}
});
function onMousedown(evt: Event) {
if (!contains(rootEl, evt.target) && (rootEl !== evt.target)) emit('closed');
}
</script>
<style lang="scss" scoped>
.nvlagfpb {
position: absolute;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
transform-origin: left top;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
transform: scale(0.9);
}
</style>

View file

@ -4,7 +4,6 @@
class="rrevdjwt"
:class="{ center: align === 'center', asDrawer }"
:style="{ width: (width && !asDrawer) ? width + 'px' : '', maxHeight: maxHeight ? maxHeight + 'px' : '' }"
@contextmenu.self="e => e.preventDefault()"
>
<template v-for="(item, i) in items2">
<div v-if="item === null" class="divider"></div>

View file

@ -1,7 +1,7 @@
<template>
<transition :name="$store.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : ''" :duration="$store.state.animation ? 200 : 0" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened">
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick"></div>
<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
<slot :max-height="maxHeight" :type="type"></slot>
</div>

View file

@ -2,7 +2,7 @@
<transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="emit('closed')">
<div v-if="showing" ref="main" class="ebkgocck">
<div class="body _shadow _narrow_" @mousedown="moveToTop" @keydown="onKeydown">
<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
<div class="header" :class="{ mini }">
<span class="left">
<button v-for="button in buttonsLeft" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
</span>
@ -70,7 +70,6 @@ const props = withDefaults(defineProps<{
closeButton?: boolean;
mini?: boolean;
front?: boolean;
contextmenu?: MenuItem[];
buttonsLeft?: any[];
buttonsRight?: any[];
}>(), {
@ -79,7 +78,6 @@ const props = withDefaults(defineProps<{
closeButton: true,
mini: false,
front: false,
contextmenu: () => [] as MenuItem[],
buttonsLeft: () => [],
buttonsRight: () => [],
});
@ -121,12 +119,6 @@ function onKeydown(evt: KeyboardEvent): void {
}
}
function onContextmenu(ev: MouseEvent): void {
if (props.contextmenu) {
os.contextMenu(props.contextmenu, ev);
}
}
function moveToTop(): void {
main.style.zIndex = os.claimZIndex(props.front ? 'middle' : 'low').toString();
}
@ -139,9 +131,6 @@ function getClickPos(evt: MouseEvent | TouchEvent): [number, number] {
}
function onHeaderMousedown(evt: MouseEvent | TouchEvent): void {
// Right-click ignored as it is likely to have attempted to open a context menu
if (evt instanceof MouseEvent && evt.button === 2) return;
if (!contains(main, document.activeElement)) main.focus();
const position = main.getBoundingClientRect();

View file

@ -171,7 +171,6 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
const app = createApp(
window.location.search === '?zen' ? defineAsyncComponent(() => import('@/ui/zen.vue')) :
!$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) :
ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) :
ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) :
defineAsyncComponent(() => import('@/ui/universal.vue')),
);

View file

@ -169,13 +169,6 @@ export const menuDef = reactive({
localStorage.setItem('ui', 'default');
unisonReload();
},
}, {
text: i18n.ts.deck,
active: ui === 'deck',
action: () => {
localStorage.setItem('ui', 'deck');
unisonReload();
},
}, {
text: i18n.ts.classic,
active: ui === 'classic',

View file

@ -527,24 +527,6 @@ export function popupMenu(items: MenuItem[] | Ref<MenuItem[]>, src?: HTMLElement
});
}
export function contextMenu(items: MenuItem[] | Ref<MenuItem[]>, ev: MouseEvent) {
ev.preventDefault();
return new Promise((resolve) => {
let dispose;
popup(defineAsyncComponent(() => import('@/components/ui/context-menu.vue')), {
items,
ev,
}, {
closed: () => {
resolve();
dispose();
},
}).then(res => {
dispose = res.dispose;
});
});
}
export function post(props: Record<string, any> = {}) {
return new Promise((resolve) => {
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
@ -564,8 +546,6 @@ export function post(props: Record<string, any> = {}) {
});
}
export const deckGlobalEvents = new EventEmitter();
/*
export function checkExistence(fileData: ArrayBuffer): Promise<any> {
return new Promise((resolve) => {

View file

@ -1,77 +0,0 @@
<template>
<div class="_formRoot">
<FormSwitch v-model="navWindow">{{ i18n.ts.defaultNavigationBehaviour }}: {{ i18n.ts.openInWindow }}</FormSwitch>
<FormSwitch v-model="alwaysShowMainColumn" class="_formBlock">{{ i18n.ts._deck.alwaysShowMainColumn }}</FormSwitch>
<FormRadios v-model="columnAlign" class="_formBlock">
<template #label>{{ i18n.ts._deck.columnAlign }}</template>
<option value="left">{{ i18n.ts.left }}</option>
<option value="center">{{ i18n.ts.center }}</option>
</FormRadios>
<FormRadios v-model="columnHeaderHeight" class="_formBlock">
<template #label>{{ i18n.ts._deck.columnHeaderHeight }}</template>
<option :value="42">{{ i18n.ts.narrow }}</option>
<option :value="45">{{ i18n.ts.medium }}</option>
<option :value="48">{{ i18n.ts.wide }}</option>
</FormRadios>
<FormInput v-model="columnMargin" type="number" class="_formBlock">
<template #label>{{ i18n.ts._deck.columnMargin }}</template>
<template #suffix>px</template>
</FormInput>
<FormLink class="_formBlock" @click="setProfile">{{ i18n.ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
</div>
</template>
<script lang="ts" setup>
import { computed, watch } from 'vue';
import FormSwitch from '@/components/form/switch.vue';
import FormLink from '@/components/form/link.vue';
import FormRadios from '@/components/form/radios.vue';
import FormInput from '@/components/form/input.vue';
import { deckStore } from '@/ui/deck/deck-store';
import * as os from '@/os';
import { unisonReload } from '@/scripts/unison-reload';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));
const columnMargin = computed(deckStore.makeGetterSetter('columnMargin'));
const columnHeaderHeight = computed(deckStore.makeGetterSetter('columnHeaderHeight'));
const profile = computed(deckStore.makeGetterSetter('profile'));
watch(navWindow, async () => {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.reloadToApplySetting,
});
if (canceled) return;
unisonReload();
});
async function setProfile() {
const { canceled, result: name } = await os.inputText({
title: i18n.ts._deck.profile,
allowEmpty: false,
});
if (canceled) return;
profile.value = name;
unisonReload();
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
definePageMetadata({
title: i18n.ts.deck,
icon: 'fas fa-columns',
});
</script>

View file

@ -26,7 +26,6 @@
<template #label>{{ i18n.ts.behavior }}</template>
<FormSwitch v-model="imageNewTab" class="_formBlock">{{ i18n.ts.openImageInNewTab }}</FormSwitch>
<FormSwitch v-model="enableInfiniteScroll" class="_formBlock">{{ i18n.ts.enableInfiniteScroll }}</FormSwitch>
<FormSwitch v-model="useReactionPickerForContextMenu" class="_formBlock">{{ i18n.ts.useReactionPickerForContextMenu }}</FormSwitch>
<FormSelect v-model="serverDisconnectedBehavior" class="_formBlock">
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
@ -90,8 +89,6 @@
<template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template>
</FormRange>
<FormLink to="/settings/deck" class="_formBlock">{{ i18n.ts.deck }}</FormLink>
<FormLink to="/settings/custom-css" class="_formBlock"><template #icon><i class="fas fa-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
</div>
</template>
@ -145,7 +142,6 @@ const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostF
const numberOfPageCache = computed(defaultStore.makeGetterSetter('numberOfPageCache'));
const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker'));
const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll'));
const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu'));
const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars'));
watch(lang, () => {

View file

@ -217,7 +217,6 @@ const component = computed(() => {
case 'menu': return defineAsyncComponent(() => import('./menu.vue'));
case 'sounds': return defineAsyncComponent(() => import('./sounds.vue'));
case 'custom-css': return defineAsyncComponent(() => import('./custom-css.vue'));
case 'deck': return defineAsyncComponent(() => import('./deck.vue'));
case 'plugin': return defineAsyncComponent(() => import('./plugin.vue'));
case 'plugin/install': return defineAsyncComponent(() => import('./plugin.install.vue'));
case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));

View file

@ -47,5 +47,9 @@ export const getNoteSummary = (note: foundkey.entities.Note): string => {
}
}
return summary.trim();
summary = summary.trim();
if (summary.length > 75) {
summary = summary.substring(0, 70) + '…';
}
return summary;
};

View file

@ -64,16 +64,8 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'deviceAccount',
default: [
'notifications',
'favorites',
'drive',
'followRequests',
'-',
'explore',
'announcements',
'search',
'-',
'ui',
],
]
},
visibility: {
where: 'deviceAccount',
@ -95,9 +87,9 @@ export const defaultStore = markRaw(new Storage('base', {
tl: {
where: 'deviceAccount',
default: {
src: 'home' as 'home' | 'local' | 'social' | 'global',
arg: null,
},
src: 'social' as 'home' | 'local' | 'social' | 'global',
arg: null
}
},
overridedDeviceKind: {
@ -164,17 +156,13 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: true,
},
useReactionPickerForContextMenu: {
where: 'device',
default: false,
},
showGapBetweenNotesInTimeline: {
where: 'device',
default: false,
},
darkMode: {
where: 'device',
default: false,
default: true
},
instanceTicker: {
where: 'device',
@ -246,8 +234,8 @@ type Plugin = {
/**
* ()
*/
import lightTheme from '@/themes/l-light.json5';
import darkTheme from '@/themes/d-dark.json5';
import lightTheme from '@/themes/l-sushi.json5';
import darkTheme from '@/themes/d-cherrybleu.json5';
export class ColdDeviceStorage {
public static default = {
@ -257,7 +245,7 @@ export class ColdDeviceStorage {
plugins: [] as Plugin[],
mediaVolume: 0.5,
sound_masterVolume: 0.3,
sound_note: { type: 'syuilo/down', volume: 1 },
sound_note: { type: null, volume: 1 },
sound_noteMy: { type: 'syuilo/up', volume: 1 },
sound_notification: { type: 'syuilo/pope2', volume: 1 },
sound_chat: { type: 'syuilo/pope1', volume: 1 },

View file

@ -0,0 +1,25 @@
{
base: 'dark',
props: {
accent: 'rgb(255, 89, 117)',
actualBg: 'rgb(28, 28, 37)',
bg: 'transparent',
mg: ':lighten<10<@actualBg',
fg: 'rgb(236, 239, 244)',
panel: 'rgb(35, 35, 47)',
renote: '@accent',
link: '@accent',
mention: '@accent',
hashtag: '@accent',
divider: 'rgb(63, 63, 80)',
wallpaperOverlay: ':alpha<0.5<@actualBg',
modalBg: ':alpha<0.5<@actualBg',
shadow: ':alpha<0.3<@actualBg',
buttonBg: '@mg',
windowHeader: '@mg',
},
id: '32b376df-9f1c-4ffc-8a2a-8f8c4a8792ca',
name: 'CherryBleu',
author: '@michcio@fedi2.0x7f.one',
desc: 'Cherry but with more transparencies',
}

View file

@ -10,7 +10,7 @@
<XWidgets :place="'left'" @mounted="attachSticky(widgetsLeft)"/>
</div>
<main class="main" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu">
<main class="main" :style="{ background: pageMetadata?.value?.bg }">
<div class="content">
<RouterView/>
</div>
@ -83,39 +83,6 @@ function attachSticky(el) {
}, { passive: true });
}
function top() {
window.scroll({ top: 0, behavior: 'smooth' });
}
function onContextmenu(ev: MouseEvent) {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(ev.target)) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
if (window.getSelection().toString() !== '') return;
const path = mainRouter.getCurrentPath();
os.contextMenu([{
type: 'label',
text: path,
}, {
icon: fullView ? 'fas fa-compress' : 'fas fa-expand',
text: fullView ? i18n.ts.quitFullView : i18n.ts.fullView,
action: () => {
fullView = !fullView;
},
}, {
icon: 'fas fa-window-maximize',
text: i18n.ts.openInWindow,
action: () => {
os.pageWindow(path);
},
}], ev);
}
if (window.innerWidth < 1024) {
localStorage.setItem('ui', 'default');
location.reload();

View file

@ -1,297 +0,0 @@
<template>
<div
class="mk-deck" :class="[{ isMobile }, `${deckStore.reactiveState.columnAlign.value}`]" :style="{ '--deckMargin': deckStore.reactiveState.columnMargin.value + 'px' }"
@contextmenu.self.prevent="onContextmenu"
>
<XSidebar v-if="!isMobile"/>
<template v-for="ids in layout">
<!-- sectionを利用しているのはdeck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
<section
v-if="ids.length > 1"
class="folder column"
:style="columns.filter(c => ids.includes(c.id)).some(c => c.flexible) ? { flex: 1, minWidth: '350px' } : { width: Math.max(...columns.filter(c => ids.includes(c.id)).map(c => c.width)) + 'px' }"
>
<DeckColumnCore v-for="id in ids" :ref="id" :key="id" :column="columns.find(c => c.id === id)" :is-stacked="true" @parent-focus="moveFocus(id, $event)"/>
</section>
<DeckColumnCore
v-else
:ref="ids[0]"
:key="ids[0]"
class="column"
:column="columns.find(c => c.id === ids[0])"
:is-stacked="false"
:style="columns.find(c => c.id === ids[0])!.flexible ? { flex: 1, minWidth: '350px' } : { width: columns.find(c => c.id === ids[0])!.width + 'px' }"
@parent-focus="moveFocus(ids[0], $event)"
/>
</template>
<div v-if="isMobile" class="buttons">
<button class="button nav _button" @click="drawerMenuShowing = true"><i class="fas fa-bars"></i><span v-if="menuIndicated" class="indicator"><i class="fas fa-circle"></i></span></button>
<button class="button home _button" @click="mainRouter.push('/')"><i class="fas fa-home"></i></button>
<button class="button notifications _button" @click="mainRouter.push('/my/notifications')"><i class="fas fa-bell"></i><span v-if="$i?.hasUnreadNotification" class="indicator"><i class="fas fa-circle"></i></span></button>
<button class="button post _button" @click="os.post()"><i class="fas fa-pencil-alt"></i></button>
</div>
<transition :name="$store.state.animation ? 'menu-back' : ''">
<div
v-if="drawerMenuShowing"
class="menu-back _modalBg"
@click="drawerMenuShowing = false"
@touchstart.passive="drawerMenuShowing = false"
></div>
</transition>
<transition :name="$store.state.animation ? 'menu' : ''">
<XDrawerMenu v-if="drawerMenuShowing" class="menu"/>
</transition>
<XCommon/>
</div>
</template>
<script lang="ts" setup>
import { computed, provide, ref, watch } from 'vue';
import { v4 as uuid } from 'uuid';
import XCommon from './_common_/common.vue';
import { deckStore, addColumn as addColumnToStore, loadDeck } from './deck/deck-store';
import DeckColumnCore from '@/ui/deck/column-core.vue';
import XSidebar from '@/ui/_common_/sidebar.vue';
import XDrawerMenu from '@/ui/_common_/sidebar-for-mobile.vue';
import { getScrollContainer } from '@/scripts/scroll';
import * as os from '@/os';
import { menuDef } from '@/menu';
import { $i } from '@/account';
import { i18n } from '@/i18n';
import { mainRouter } from '@/router';
if (deckStore.state.navWindow) {
mainRouter.navHook = (path) => {
os.pageWindow(path);
return true;
};
}
const isMobile = ref(window.innerWidth <= 500);
window.addEventListener('resize', () => {
isMobile.value = window.innerWidth <= 500;
});
const drawerMenuShowing = ref(false);
const route = 'TODO';
watch(route, () => {
drawerMenuShowing.value = false;
});
const columns = deckStore.reactiveState.columns;
const layout = deckStore.reactiveState.layout;
const menuIndicated = computed(() => {
if ($i == null) return false;
for (const def in menuDef) {
if (menuDef[def].indicated) return true;
}
return false;
});
const addColumn = async (ev) => {
const columns = [
'main',
'widgets',
'notifications',
'tl',
'antenna',
'list',
'mentions',
'direct',
];
const { canceled, result: column } = await os.select({
title: i18n.ts._deck.addColumn,
items: columns.map(column => ({
value: column, text: i18n.t('_deck._columns.' + column),
})),
});
if (canceled) return;
addColumnToStore({
type: column,
id: uuid(),
name: i18n.t('_deck._columns.' + column),
width: 330,
});
};
const onContextmenu = (ev) => {
os.contextMenu([{
text: i18n.ts._deck.addColumn,
action: addColumn,
}], ev);
};
provide('shouldSpacerMin', true);
document.documentElement.style.overflowY = 'hidden';
document.documentElement.style.scrollBehavior = 'auto';
window.addEventListener('wheel', (ev) => {
if (getScrollContainer(ev.target as HTMLElement) == null && ev.deltaX === 0) {
document.documentElement.scrollLeft += ev.deltaY;
}
});
loadDeck();
function moveFocus(id: string, direction: 'up' | 'down' | 'left' | 'right') {
// TODO??
}
</script>
<style lang="scss" scoped>
.menu-enter-active,
.menu-leave-active {
opacity: 1;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.menu-enter-from,
.menu-leave-active {
opacity: 0;
transform: translateX(-240px);
}
.menu-back-enter-active,
.menu-back-leave-active {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.menu-back-enter-from,
.menu-back-leave-active {
opacity: 0;
}
.mk-deck {
$nav-hide-threshold: 650px; // TODO:
// TODO:
--margin: var(--marginHalf);
display: flex;
// 100vh ... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
height: calc(var(--vh, 1vh) * 100);
box-sizing: border-box;
flex: 1;
padding: var(--deckMargin);
&.center {
> .column:first-of-type {
margin-left: auto;
}
> .column:last-of-type {
margin-right: auto;
}
}
&.isMobile {
padding-bottom: 100px;
}
> .column {
flex-shrink: 0;
margin-right: var(--deckMargin);
&.folder {
display: flex;
flex-direction: column;
> *:not(:last-child) {
margin-bottom: var(--deckMargin);
}
}
}
> .buttons {
position: fixed;
z-index: 1000;
bottom: 0;
left: 0;
padding: 16px;
display: flex;
width: 100%;
box-sizing: border-box;
> .button {
position: relative;
flex: 1;
padding: 0;
margin: auto;
height: 64px;
border-radius: 8px;
background: var(--panel);
color: var(--fg);
&:not(:last-child) {
margin-right: 12px;
}
@media (max-width: 400px) {
height: 60px;
&:not(:last-child) {
margin-right: 8px;
}
}
&:hover {
background: var(--X2);
}
> .indicator {
position: absolute;
top: 0;
left: 0;
color: var(--indicator);
font-size: 16px;
animation: blink 1s infinite;
}
&:first-child {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
> * {
font-size: 20px;
}
&:disabled {
cursor: default;
> * {
opacity: 0.5;
}
}
}
}
> .menu-back {
z-index: 1001;
}
> .menu {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
// 100vh ... https://css-tricks.com/the-trick-to-viewport-units-on-mobile/
height: calc(var(--vh, 1vh) * 100);
width: 240px;
box-sizing: border-box;
overflow: auto;
overscroll-behavior: contain;
background: var(--bg);
}
}
</style>

View file

@ -1,63 +0,0 @@
<template>
<XColumn :func="{ handler: setAntenna, title: $ts.selectAntenna }" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
<template #header>
<i class="fas fa-satellite"></i><span style="margin-left: 8px;">{{ column.name }}</span>
</template>
<XTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @after="() => emit('loaded')"/>
</XColumn>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import XColumn from './column.vue';
import { updateColumn, Column } from './deck-store';
import XTimeline from '@/components/timeline.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
const props = defineProps<{
column: Column;
isStacked: boolean;
}>();
const emit = defineEmits<{
(ev: 'loaded'): void;
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
let timeline = $ref<InstanceType<typeof XTimeline>>();
onMounted(() => {
if (props.column.antennaId == null) {
setAntenna();
}
});
async function setAntenna() {
const antennas = await os.api('antennas/list');
const { canceled, result: antenna } = await os.select({
title: i18n.ts.selectAntenna,
items: antennas.map(x => ({
value: x, text: x.name,
})),
default: props.column.antennaId,
});
if (canceled) return;
updateColumn(props.column.id, {
antennaId: antenna.id,
});
}
/*
function focus() {
timeline.focus();
}
defineExpose({
focus,
});
*/
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,43 +0,0 @@
<template>
<!-- TODO: リファクタの余地がありそう -->
<div v-if="!column">たぶん見えちゃいけないやつ</div>
<XMainColumn v-else-if="column.type === 'main'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
<XWidgetsColumn v-else-if="column.type === 'widgets'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
<XNotificationsColumn v-else-if="column.type === 'notifications'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
<XTlColumn v-else-if="column.type === 'tl'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
<XListColumn v-else-if="column.type === 'list'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
<XAntennaColumn v-else-if="column.type === 'antenna'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
<XMentionsColumn v-else-if="column.type === 'mentions'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
<XDirectColumn v-else-if="column.type === 'direct'" :column="column" :is-stacked="isStacked" @parent-focus="emit('parent-focus', $event)"/>
</template>
<script lang="ts" setup>
import XMainColumn from './main-column.vue';
import XTlColumn from './tl-column.vue';
import XAntennaColumn from './antenna-column.vue';
import XListColumn from './list-column.vue';
import XNotificationsColumn from './notifications-column.vue';
import XWidgetsColumn from './widgets-column.vue';
import XMentionsColumn from './mentions-column.vue';
import XDirectColumn from './direct-column.vue';
import { Column } from './deck-store';
defineProps<{
column?: Column;
isStacked: boolean;
}>();
const emit = defineEmits<{
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
/*
export default defineComponent({
methods: {
focus() {
this.$children[0].focus();
}
}
});
*/
</script>

View file

@ -1,382 +0,0 @@
<template>
<!-- sectionを利用しているのはdeck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
<section
v-hotkey="keymap" class="dnpfarvg _panel _narrow_"
:class="{ paged: isMainColumn, naked, active, isStacked, draghover, dragging, dropready }"
:style="{ '--deckColumnHeaderHeight': deckStore.reactiveState.columnHeaderHeight.value + 'px' }"
@dragover.prevent.stop="onDragover"
@dragleave="onDragleave"
@drop.prevent.stop="onDrop"
>
<header
:class="{ indicated }"
draggable="true"
@click="goTop"
@dragstart="onDragstart"
@dragend="onDragend"
@contextmenu.prevent.stop="onContextmenu"
>
<button v-if="isStacked && !isMainColumn" class="toggleActive _button" @click="toggleActive">
<template v-if="active"><i class="fas fa-angle-up"></i></template>
<template v-else><i class="fas fa-angle-down"></i></template>
</button>
<div class="action">
<slot name="action"></slot>
</div>
<span class="header"><slot name="header"></slot></span>
<button v-if="func" v-tooltip="func.title" class="menu _button" @click.stop="func.handler"><i :class="func.icon || 'fas fa-cog'"></i></button>
</header>
<div v-show="active" ref="body">
<slot></slot>
</div>
</section>
</template>
<script lang="ts">
export type DeckFunc = {
title: string;
handler: (payload: MouseEvent) => void;
icon?: string;
};
</script>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, provide, watch } from 'vue';
import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn, Column , deckStore } from './deck-store';
import * as os from '@/os';
import { i18n } from '@/i18n';
provide('shouldHeaderThin', true);
provide('shouldOmitHeaderTitle', true);
const props = withDefaults(defineProps<{
column: Column;
isStacked?: boolean;
func?: DeckFunc | null;
naked?: boolean;
indicated?: boolean;
}>(), {
isStacked: false,
func: null,
naked: false,
indicated: false,
});
const emit = defineEmits<{
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
(ev: 'change-active-state', v: boolean): void;
}>();
let body = $ref<HTMLDivElement>();
let dragging = $ref(false);
watch($$(dragging), v => os.deckGlobalEvents.emit(v ? 'column.dragStart' : 'column.dragEnd'));
let draghover = $ref(false);
let dropready = $ref(false);
const isMainColumn = $computed(() => props.column.type === 'main');
const active = $computed(() => props.column.active !== false);
watch($$(active), v => emit('change-active-state', v));
const keymap = $computed(() => ({
'shift+up': () => emit('parent-focus', 'up'),
'shift+down': () => emit('parent-focus', 'down'),
'shift+left': () => emit('parent-focus', 'left'),
'shift+right': () => emit('parent-focus', 'right'),
}));
onMounted(() => {
os.deckGlobalEvents.on('column.dragStart', onOtherDragStart);
os.deckGlobalEvents.on('column.dragEnd', onOtherDragEnd);
});
onBeforeUnmount(() => {
os.deckGlobalEvents.off('column.dragStart', onOtherDragStart);
os.deckGlobalEvents.off('column.dragEnd', onOtherDragEnd);
});
function onOtherDragStart() {
dropready = true;
}
function onOtherDragEnd() {
dropready = false;
}
function toggleActive() {
if (!props.isStacked) return;
updateColumn(props.column.id, {
active: !props.column.active,
});
}
function getMenu() {
const items = [{
icon: 'fas fa-pencil-alt',
text: i18n.ts.edit,
action: async () => {
const { canceled, result } = await os.form(props.column.name, {
name: {
type: 'string',
label: i18n.ts.name,
default: props.column.name,
},
width: {
type: 'number',
label: i18n.ts.width,
default: props.column.width,
},
flexible: {
type: 'boolean',
label: i18n.ts.flexible,
default: props.column.flexible,
},
});
if (canceled) return;
updateColumn(props.column.id, result);
},
}, null, {
icon: 'fas fa-arrow-left',
text: i18n.ts._deck.swapLeft,
action: () => {
swapLeftColumn(props.column.id);
},
}, {
icon: 'fas fa-arrow-right',
text: i18n.ts._deck.swapRight,
action: () => {
swapRightColumn(props.column.id);
},
}, props.isStacked ? {
icon: 'fas fa-arrow-up',
text: i18n.ts._deck.swapUp,
action: () => {
swapUpColumn(props.column.id);
},
} : undefined, props.isStacked ? {
icon: 'fas fa-arrow-down',
text: i18n.ts._deck.swapDown,
action: () => {
swapDownColumn(props.column.id);
},
} : undefined, null, {
icon: 'fas fa-window-restore',
text: i18n.ts._deck.stackLeft,
action: () => {
stackLeftColumn(props.column.id);
},
}, props.isStacked ? {
icon: 'fas fa-window-maximize',
text: i18n.ts._deck.popRight,
action: () => {
popRightColumn(props.column.id);
},
} : undefined, null, {
icon: 'fas fa-trash-alt',
text: i18n.ts.remove,
danger: true,
action: () => {
removeColumn(props.column.id);
},
}];
return items;
}
function onContextmenu(ev: MouseEvent) {
os.contextMenu(getMenu(), ev);
}
function goTop() {
body.scrollTo({
top: 0,
behavior: 'smooth',
});
}
function onDragstart(ev) {
ev.dataTransfer.effectAllowed = 'move';
ev.dataTransfer.setData(_DATA_TRANSFER_DECK_COLUMN_, props.column.id);
// ChromeDragstartDOM(=)Drag
// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
window.setTimeout(() => {
dragging = true;
}, 10);
}
function onDragend(ev) {
dragging = false;
}
function onDragover(ev) {
//
if (dragging) {
//
ev.dataTransfer.dropEffect = 'none';
} else {
const isDeckColumn = ev.dataTransfer.types[0] === _DATA_TRANSFER_DECK_COLUMN_;
ev.dataTransfer.dropEffect = isDeckColumn ? 'move' : 'none';
if (isDeckColumn) draghover = true;
}
}
function onDragleave() {
draghover = false;
}
function onDrop(ev) {
draghover = false;
os.deckGlobalEvents.emit('column.dragEnd');
const id = ev.dataTransfer.getData(_DATA_TRANSFER_DECK_COLUMN_);
if (id != null && id !== '') {
swapColumn(props.column.id, id);
}
}
</script>
<style lang="scss" scoped>
.dnpfarvg {
--root-margin: 10px;
height: 100%;
overflow: hidden;
contain: content;
box-shadow: 0 0 8px 0 var(--shadow);
&.draghover {
box-shadow: 0 0 0 2px var(--focus);
&:after {
content: "";
display: block;
position: absolute;
z-index: 1000;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--focus);
}
}
&.dragging {
box-shadow: 0 0 0 2px var(--focus);
}
&.dropready {
* {
pointer-events: none;
}
}
&:not(.active) {
flex-basis: var(--deckColumnHeaderHeight);
min-height: var(--deckColumnHeaderHeight);
> header.indicated {
box-shadow: 4px 0px var(--accent) inset;
}
}
&.naked {
background: var(--bg) !important;
-webkit-backdrop-filter: var(--blur, blur(10px));
backdrop-filter: var(--blur, blur(10px));
> header {
background: transparent;
box-shadow: none;
> button {
color: var(--fg);
}
}
}
&.paged {
background: var(--bg) !important;
}
> header {
position: relative;
display: flex;
z-index: 2;
line-height: var(--deckColumnHeaderHeight);
height: var(--deckColumnHeaderHeight);
padding: 0 16px;
font-size: 0.9em;
color: var(--panelHeaderFg);
background: var(--panelHeaderBg);
box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
cursor: pointer;
&, * {
user-select: none;
}
&.indicated {
box-shadow: 0 3px 0 0 var(--accent);
}
> .header {
display: inline-block;
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
> span:only-of-type {
width: 100%;
}
> .toggleActive,
> .action > ::v-deep(*),
> .menu {
z-index: 1;
width: var(--deckColumnHeaderHeight);
line-height: var(--deckColumnHeaderHeight);
font-size: 16px;
color: var(--faceTextButton);
&:hover {
color: var(--faceTextButtonHover);
}
&:active {
color: var(--faceTextButtonActive);
}
}
> .toggleActive, > .action {
margin-left: -16px;
}
> .action {
z-index: 1;
}
> .action:empty {
display: none;
}
> .menu {
margin-left: auto;
margin-right: -16px;
}
}
> div {
height: calc(100% - var(--deckColumnHeaderHeight));
overflow-y: auto;
overflow-x: hidden; // Safari does not supports clip
overflow-x: clip;
-webkit-overflow-scrolling: touch;
box-sizing: border-box;
}
}
</style>

View file

@ -1,304 +0,0 @@
import { throttle } from 'throttle-debounce';
import { markRaw } from 'vue';
import { notificationTypes } from 'foundkey-js';
import { Storage } from '../../pizzax';
import { i18n } from '@/i18n';
import { api } from '@/os';
type ColumnWidget = {
name: string;
id: string;
data: Record<string, any>;
};
export type Column = {
id: string;
type: string;
name: string | null;
width: number;
widgets?: ColumnWidget[];
active?: boolean;
flexible?: boolean;
antennaId?: string;
listId?: string;
includingTypes?: typeof notificationTypes[number][];
tl?: 'home' | 'local' | 'social' | 'global';
};
function copy<T>(x: T): T {
return JSON.parse(JSON.stringify(x));
}
export const deckStore = markRaw(new Storage('deck', {
profile: {
where: 'deviceAccount',
default: 'default',
},
columns: {
where: 'deviceAccount',
default: [] as Column[],
},
layout: {
where: 'deviceAccount',
default: [] as Column['id'][][],
},
columnAlign: {
where: 'deviceAccount',
default: 'left' as 'left' | 'right' | 'center',
},
alwaysShowMainColumn: {
where: 'deviceAccount',
default: true,
},
navWindow: {
where: 'deviceAccount',
default: true,
},
columnMargin: {
where: 'deviceAccount',
default: 16,
},
columnHeaderHeight: {
where: 'deviceAccount',
default: 42,
},
}));
export const loadDeck = async () => {
let deck;
try {
deck = await api('i/registry/get', {
scope: ['client', 'deck', 'profiles'],
key: deckStore.state.profile,
});
} catch (err) {
if (err.code === 'NO_SUCH_KEY') {
// 後方互換性のため
if (deckStore.state.profile === 'default') {
saveDeck();
return;
}
deckStore.set('columns', [{
id: 'a',
type: 'main',
name: i18n.ts._deck._columns.main,
width: 350,
}, {
id: 'b',
type: 'notifications',
name: i18n.ts._deck._columns.notifications,
width: 330,
}]);
deckStore.set('layout', [['a'], ['b']]);
return;
}
throw err;
}
deckStore.set('columns', deck.columns);
deckStore.set('layout', deck.layout);
};
// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
export const saveDeck = throttle(1000, () => {
api('i/registry/set', {
scope: ['client', 'deck', 'profiles'],
key: deckStore.state.profile,
value: {
columns: deckStore.reactiveState.columns.value,
layout: deckStore.reactiveState.layout.value,
},
});
});
export function addColumn(column: Column) {
if (column.name === undefined) column.name = null;
deckStore.push('columns', column);
deckStore.push('layout', [column.id]);
saveDeck();
}
export function removeColumn(id: Column['id']) {
deckStore.set('columns', deckStore.state.columns.filter(c => c.id !== id));
deckStore.set('layout', deckStore.state.layout
.map(ids => ids.filter(_id => _id !== id))
.filter(ids => ids.length > 0));
saveDeck();
}
export function swapColumn(a: Column['id'], b: Column['id']) {
const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) !== -1);
const aY = deckStore.state.layout[aX].findIndex(id => id === a);
const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1);
const bY = deckStore.state.layout[bX].findIndex(id => id === b);
const layout = copy(deckStore.state.layout);
layout[aX][aY] = b;
layout[bX][bY] = a;
deckStore.set('layout', layout);
saveDeck();
}
export function swapLeftColumn(id: Column['id']) {
const layout = copy(deckStore.state.layout);
deckStore.state.layout.some((ids, i) => {
if (ids.includes(id)) {
const left = deckStore.state.layout[i - 1];
if (left) {
layout[i - 1] = deckStore.state.layout[i];
layout[i] = left;
deckStore.set('layout', layout);
}
return true;
}
});
saveDeck();
}
export function swapRightColumn(id: Column['id']) {
const layout = copy(deckStore.state.layout);
deckStore.state.layout.some((ids, i) => {
if (ids.includes(id)) {
const right = deckStore.state.layout[i + 1];
if (right) {
layout[i + 1] = deckStore.state.layout[i];
layout[i] = right;
deckStore.set('layout', layout);
}
return true;
}
});
saveDeck();
}
export function swapUpColumn(id: Column['id']) {
const layout = copy(deckStore.state.layout);
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
const ids = copy(deckStore.state.layout[idsIndex]);
ids.some((x, i) => {
if (x === id) {
const up = ids[i - 1];
if (up) {
ids[i - 1] = id;
ids[i] = up;
layout[idsIndex] = ids;
deckStore.set('layout', layout);
}
return true;
}
});
saveDeck();
}
export function swapDownColumn(id: Column['id']) {
const layout = copy(deckStore.state.layout);
const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
const ids = copy(deckStore.state.layout[idsIndex]);
ids.some((x, i) => {
if (x === id) {
const down = ids[i + 1];
if (down) {
ids[i + 1] = id;
ids[i] = down;
layout[idsIndex] = ids;
deckStore.set('layout', layout);
}
return true;
}
});
saveDeck();
}
export function stackLeftColumn(id: Column['id']) {
let layout = copy(deckStore.state.layout);
const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
layout = layout.map(ids => ids.filter(_id => _id !== id));
layout[i - 1].push(id);
layout = layout.filter(ids => ids.length > 0);
deckStore.set('layout', layout);
saveDeck();
}
export function popRightColumn(id: Column['id']) {
let layout = copy(deckStore.state.layout);
const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
const affected = layout[i];
layout = layout.map(ids => ids.filter(_id => _id !== id));
layout.splice(i + 1, 0, [id]);
layout = layout.filter(ids => ids.length > 0);
deckStore.set('layout', layout);
const columns = copy(deckStore.state.columns);
for (const column of columns) {
if (affected.includes(column.id)) {
column.active = true;
}
}
deckStore.set('columns', columns);
saveDeck();
}
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
const columns = copy(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const column = copy(deckStore.state.columns[columnIndex]);
if (column == null) return;
if (column.widgets == null) column.widgets = [];
column.widgets.unshift(widget);
columns[columnIndex] = column;
deckStore.set('columns', columns);
saveDeck();
}
export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
const columns = copy(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const column = copy(deckStore.state.columns[columnIndex]);
if (column == null) return;
column.widgets = column.widgets.filter(w => w.id !== widget.id);
columns[columnIndex] = column;
deckStore.set('columns', columns);
saveDeck();
}
export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
const columns = copy(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const column = copy(deckStore.state.columns[columnIndex]);
if (column == null) return;
column.widgets = widgets;
columns[columnIndex] = column;
deckStore.set('columns', columns);
saveDeck();
}
export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
const columns = copy(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const column = copy(deckStore.state.columns[columnIndex]);
if (column == null) return;
column.widgets = column.widgets.map(w => w.id === widgetId ? {
...w,
data: widgetData,
} : w);
columns[columnIndex] = column;
deckStore.set('columns', columns);
saveDeck();
}
export function updateColumn(id: Column['id'], column: Partial<Column>) {
const columns = copy(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
const currentColumn = copy(deckStore.state.columns[columnIndex]);
if (currentColumn == null) return;
for (const [k, v] of Object.entries(column)) {
currentColumn[k] = v;
}
columns[columnIndex] = currentColumn;
deckStore.set('columns', columns);
saveDeck();
}

View file

@ -1,30 +0,0 @@
<template>
<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
<template #header><i class="fas fa-envelope" style="margin-right: 8px;"></i>{{ column.name }}</template>
<XNotes :pagination="pagination"/>
</XColumn>
</template>
<script lang="ts" setup>
import XColumn from './column.vue';
import { Column } from './deck-store';
import XNotes from '@/components/notes.vue';
defineProps<{
column: Column;
isStacked: boolean;
}>();
const emit = defineEmits<{
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
const pagination = {
endpoint: 'notes/mentions' as const,
limit: 10,
params: {
visibility: 'specified',
},
};
</script>

View file

@ -1,65 +0,0 @@
<template>
<XColumn :func="{ handler: setList, title: $ts.selectList }" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
<template #header>
<i class="fas fa-list-ul"></i><span style="margin-left: 8px;">{{ column.name }}</span>
</template>
<XTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" @after="() => emit('loaded')"/>
</XColumn>
</template>
<script lang="ts" setup>
import XColumn from './column.vue';
import { updateColumn, Column } from './deck-store';
import XTimeline from '@/components/timeline.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
const props = defineProps<{
column: Column;
isStacked: boolean;
}>();
const emit = defineEmits<{
(ev: 'loaded'): void;
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
let timeline = $ref<InstanceType<typeof XTimeline>>();
if (props.column.listId == null) {
setList();
}
async function setList() {
const lists = await os.api('users/lists/list');
const { canceled, result: list } = await os.select({
title: i18n.ts.selectList,
items: lists.map(x => ({
value: x, text: x.name,
})),
default: props.column.listId,
});
if (canceled) return;
updateColumn(props.column.id, {
listId: list.id,
});
}
/*
function focus() {
timeline.focus();
}
export default defineComponent({
watch: {
mediaOnly() {
(this.$refs.timeline as any).reload();
}
}
});
*/
</script>
<style lang="scss" scoped>
</style>

View file

@ -1,68 +0,0 @@
<template>
<XColumn v-if="deckStore.state.alwaysShowMainColumn || mainRouter.currentRoute.value.name !== 'index'" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
<template #header>
<template v-if="pageMetadata?.value">
<i :class="pageMetadata?.value.icon"></i>
{{ pageMetadata?.value.title }}
</template>
</template>
<RouterView @contextmenu.stop="onContextmenu"/>
</XColumn>
</template>
<script lang="ts" setup>
import { ComputedRef, provide } from 'vue';
import XColumn from './column.vue';
import { deckStore, Column } from '@/ui/deck/deck-store';
import * as os from '@/os';
import { i18n } from '@/i18n';
import { mainRouter } from '@/router';
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
defineProps<{
column: Column;
isStacked: boolean;
}>();
const emit = defineEmits<{
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
let pageMetadata = $ref<null | ComputedRef<PageMetadata>>();
provide('router', mainRouter);
provideMetadataReceiver((info) => {
pageMetadata = info;
});
/*
function back() {
history.back();
}
*/
function onContextmenu(ev: MouseEvent) {
if (!ev.target) return;
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(ev.target as HTMLElement)) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes((ev.target as HTMLElement).tagName) || (ev.target as HTMLElement).attributes['contenteditable']) return;
if (window.getSelection()?.toString() !== '') return;
const path = mainRouter.currentRoute.value.path;
os.contextMenu([{
type: 'label',
text: path,
}, {
icon: 'fas fa-window-maximize',
text: i18n.ts.openInWindow,
action: () => {
os.pageWindow(path);
},
}], ev);
}
</script>

View file

@ -1,27 +0,0 @@
<template>
<XColumn :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
<template #header><i class="fas fa-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
<XNotes :pagination="pagination"/>
</XColumn>
</template>
<script lang="ts" setup>
import XColumn from './column.vue';
import { Column } from './deck-store';
import XNotes from '@/components/notes.vue';
defineProps<{
column: Column;
isStacked: boolean;
}>();
const emit = defineEmits<{
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
const pagination = {
endpoint: 'notes/mentions' as const,
limit: 10,
};
</script>

View file

@ -1,37 +0,0 @@
<template>
<XColumn :column="column" :is-stacked="isStacked" :func="{ handler: func, title: $ts.notificationSetting }" @parent-focus="$event => emit('parent-focus', $event)">
<template #header><i class="fas fa-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
<XNotifications :include-types="column.includingTypes"/>
</XColumn>
</template>
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import XColumn from './column.vue';
import { updateColumn , Column } from './deck-store';
import XNotifications from '@/components/notifications.vue';
import * as os from '@/os';
const props = defineProps<{
column: Column;
isStacked: boolean;
}>();
const emit = defineEmits<{
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
function func() {
os.popup(defineAsyncComponent(() => import('@/components/notification-setting-window.vue')), {
includingTypes: props.column.includingTypes,
}, {
done: async (res) => {
const { includingTypes } = res;
updateColumn(props.column.id, {
includingTypes,
});
},
}, 'closed');
}
</script>

View file

@ -1,129 +0,0 @@
<template>
<XColumn :func="{ handler: setType, title: $ts.timeline }" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState" @parent-focus="$event => emit('parent-focus', $event)">
<template #header>
<i v-if="column.tl === 'home'" class="fas fa-home"></i>
<i v-else-if="column.tl === 'local'" class="fas fa-comments"></i>
<i v-else-if="column.tl === 'social'" class="fas fa-share-alt"></i>
<i v-else-if="column.tl === 'global'" class="fas fa-globe"></i>
<span style="margin-left: 8px;">{{ column.name }}</span>
</template>
<div v-if="disabled" class="iwaalbte">
<p>
<i class="fas fa-minus-circle"></i>
{{ $t('disabled-timeline.title') }}
</p>
<p class="desc">{{ $t('disabled-timeline.description') }}</p>
</div>
<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')" @queue="queueUpdated" @note="onNote"/>
</XColumn>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import XColumn from './column.vue';
import { removeColumn, updateColumn, Column } from './deck-store';
import XTimeline from '@/components/timeline.vue';
import * as os from '@/os';
import { $i } from '@/account';
import { instance } from '@/instance';
import { i18n } from '@/i18n';
const props = defineProps<{
column: Column;
isStacked: boolean;
}>();
const emit = defineEmits<{
(ev: 'loaded'): void;
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
let disabled = $ref(false);
let indicated = $ref(false);
let columnActive = $ref(true);
onMounted(() => {
if (props.column.tl == null) {
setType();
} else if ($i) {
disabled = !$i.isModerator && !$i.isAdmin && (
instance.disableLocalTimeline && ['local', 'social'].includes(props.column.tl) ||
instance.disableGlobalTimeline && ['global'].includes(props.column.tl));
}
});
async function setType() {
const { canceled, result: src } = await os.select({
title: i18n.ts.timeline,
items: [{
value: 'home' as const, text: i18n.ts._timelines.home,
}, {
value: 'local' as const, text: i18n.ts._timelines.local,
}, {
value: 'social' as const, text: i18n.ts._timelines.social,
}, {
value: 'global' as const, text: i18n.ts._timelines.global,
}],
});
if (canceled) {
if (props.column.tl == null) {
removeColumn(props.column.id);
}
return;
}
updateColumn(props.column.id, {
tl: src,
});
}
function queueUpdated(q) {
if (columnActive) {
indicated = q !== 0;
}
}
function onNote() {
if (!columnActive) {
indicated = true;
}
}
function onChangeActiveState(state) {
columnActive = state;
if (columnActive) {
indicated = false;
}
}
/*
export default defineComponent({
watch: {
mediaOnly() {
(this.$refs.timeline as any).reload();
}
},
methods: {
focus() {
(this.$refs.timeline as any).focus();
}
}
});
*/
</script>
<style lang="scss" scoped>
.iwaalbte {
text-align: center;
> p {
margin: 16px;
&.desc {
font-size: 14px;
}
}
}
</style>

View file

@ -1,55 +0,0 @@
<template>
<XColumn :func="{ handler: func, title: $ts.editWidgets }" :naked="true" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
<template #header><i class="fas fa-window-maximize" style="margin-right: 8px;"></i>{{ column.name }}</template>
<div class="wtdtxvec">
<XWidgets :edit="edit" :widgets="column.widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
</div>
</XColumn>
</template>
<script lang="ts" setup>
import XColumn from './column.vue';
import { addColumnWidget, Column, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store';
import XWidgets from '@/components/widgets.vue';
const props = defineProps<{
column: Column;
isStacked: boolean;
}>();
const emit = defineEmits<{
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
let edit = $ref(false);
function addWidget(widget) {
addColumnWidget(props.column.id, widget);
}
function removeWidget(widget) {
removeColumnWidget(props.column.id, widget);
}
function updateWidget({ id, data }) {
updateColumnWidget(props.column.id, id, data);
}
function updateWidgets(widgets) {
setColumnWidgets(props.column.id, widgets);
}
function func() {
edit = !edit;
}
</script>
<style lang="scss" scoped>
.wtdtxvec {
--margin: 8px;
--panelBorder: none;
padding: 0 var(--margin);
}
</style>

View file

@ -2,7 +2,7 @@
<div class="dkgtipfy" :class="{ wallpaper }">
<XSidebar v-if="!isMobile" class="sidebar"/>
<div class="contents" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu">
<div class="contents" :style="{ background: pageMetadata?.value?.bg }">
<main>
<div class="content">
<RouterView/>
@ -131,29 +131,6 @@ onMounted(() => {
}
});
const onContextmenu = (ev) => {
const isLink = (el: HTMLElement) => {
if (el.tagName === 'A') return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(ev.target)) return;
if (['INPUT', 'TEXTAREA', 'IMG', 'VIDEO', 'CANVAS'].includes(ev.target.tagName) || ev.target.attributes['contenteditable']) return;
if (window.getSelection()?.toString() !== '') return;
const path = mainRouter.getCurrentPath();
os.contextMenu([{
type: 'label',
text: path,
}, {
icon: 'fas fa-window-maximize',
text: i18n.ts.openInWindow,
action: () => {
os.pageWindow(path);
},
}], ev);
};
const attachSticky = (el) => {
const sticky = new StickySidebar(widgetsEl);
window.addEventListener('scroll', () => {

View file

@ -47,10 +47,8 @@ export default defineConfig(({ command, mode }) => {
build: {
target: [
'chrome100',
'firefox100',
'firefox102',
'safari15',
'es2017',
],
manifest: 'manifest.json',
rollupOptions: {

1242
yarn.lock

File diff suppressed because it is too large Load diff