Merge branch 'develop' into fetch

This commit is contained in:
tamaina 2023-01-11 14:55:14 +00:00
commit 3a2d9f4823
106 changed files with 2020 additions and 1910 deletions

View File

@ -31,6 +31,7 @@ You should also include the user name that made the change.
#### For users
- ノートのウォッチ機能が削除されました
- アンケートに投票された際に通知が作成されなくなりました
- ノートの数式埋め込みが削除されました
- 新たに動的なPagesを作ることはできなくなりました
- 代わりにAiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能が実装されています。
- AiScriptが0.12.2にアップデートされました
@ -61,12 +62,14 @@ You should also include the user name that made the change.
- Server: signToActivityPubGet is set to true by default @syuilo
- Server: improve syslog performance @syuilo
- Server: improve note scoring for featured notes @CyberRex0
- Server: アンケート選択肢の文字数制限を緩和 @syuilo
- Server: improve stats api performance @syuilo
- Server: improve nodeinfo performance @syuilo
- Server: delete outdated notifications regularly to improve db performance @syuilo
- Server: delete outdated hard-mutes regularly to improve db performance @syuilo
- Server: delete outdated notes of antenna regularly to improve db performance @syuilo
- Server: improve activitypub deliver performance @syuilo
- Client: use tabler-icons instead of fontawesome to better design @syuilo
- Client: Add AiScript App widget
- Client: Add new gabber kick sounds (thanks for noizenecio)
- Client: Add link to user RSS feed in profile menu @ssmucny
- Client: Compress non-animated PNG files @saschanaz
@ -74,15 +77,18 @@ You should also include the user name that made the change.
- Client: enhance dashboard of control panel @syuilo
- Client: Vite is upgraded to v4 @syuilo, @tamaina
- Client: HMR is available while yarn dev @tamaina
- Client: Make widgets of universal/classic sync between devices @tamaina
- Client: Implement the button to subscribe push notification @tamaina
- Client: Implement the toggle to or not to close push notifications when notifications or messages are read @tamaina
- Client: Improve RSS widget @tamaina
- Client: show Unicode emoji tooltip with its name in MkReactionsViewer.reaction @saschanaz
- Client: OpenSearch support @SoniEx2 @chaoticryptidz
- Client: Support remote objects in search @SoniEx2
- Client: user activity page @syuilo
- Client: Make widgets of universal/classic sync between devices @tamaina
- Client: add user list widget @syuilo
- Client: Add AiScript App widget
- Client: add profile widget @syuilo
- Client: add instance info widget @syuilo
- Client: Improve RSS widget @tamaina
- Client: add heatmap of daily active users to about page @syuilo
- Client: introduce fluent emoji @syuilo
- Client: add new theme @syuilo
@ -107,6 +113,7 @@ You should also include the user name that made the change.
- Server: アンテナタイムライン(ストリーミング)が、フォローしていないユーザーの鍵投稿も拾ってしまう @syuilo
- Client: 日付形式の文字列などがカスタム絵文字として表示されるのを修正 @syuilo
- Client: case insensitive emoji search @saschanaz
- Client: 画面の幅が狭いとウィジェットドロワーを閉じる手段がなくなるのを修正 @syuilo
- Client: InAppウィンドウが操作できなくなることがあるのを修正 @tamaina
- Client: use proxied image for instance icon @syuilo
- Client: Webhookの編集画面で、内容を保存することができない問題を修正 @m-hayabusa
@ -115,6 +122,11 @@ You should also include the user name that made the change.
- Client: チャートのツールチップが画面に残ることがあるのを修正 @syuilo
- Client: fix wrong link in tutorial @syuilo
### Special thanks
- All contributors
- All who have created instances for the beta test
- All who participated in the beta test
## 12.119.1 (2022/12/03)
### Bugfixes
- Server: Mitigate AP reference chain DoS vector @skehmatics

View File

@ -1,4 +1,4 @@
FROM node:18.12.1-bullseye AS builder
FROM node:18.13.0-bullseye AS builder
ARG NODE_ENV=production
@ -22,7 +22,7 @@ COPY . ./
RUN git submodule update --init
RUN yarn build
FROM node:18.12.1-bullseye-slim AS runner
FROM node:18.13.0-bullseye-slim AS runner
WORKDIR /misskey

View File

@ -1117,6 +1117,8 @@ _weekday:
friday: "الجمعة"
saturday: "السبت"
_widgets:
profile: "الملف التعريفي"
instanceInfo: "معلومات مثيل الخادم"
memo: "ملاحظة لاصقة"
notifications: "الإشعارات"
timeline: "الخيط الزمني"

View File

@ -1200,6 +1200,8 @@ _weekday:
friday: "শুক্রবার"
saturday: "শনিবার"
_widgets:
profile: "প্রোফাইল"
instanceInfo: "ইন্সট্যান্সের তথ্য"
memo: "স্টিকি নোট"
notifications: "বিজ্ঞপ্তি"
timeline: "টাইমলাইন"

View File

@ -399,6 +399,8 @@ _antennaSources:
userList: "Publicacions d'una llista d'usuaris"
userGroup: "Publicacions d'usuaris d'un grup"
_widgets:
profile: "Perfil"
instanceInfo: "Informació del fitxer d'instal·lació"
notifications: "Notificacions"
timeline: "Línia de temps"
activity: "Activitat"

View File

@ -694,6 +694,8 @@ _weekday:
friday: "Pátek"
saturday: "Sobota"
_widgets:
profile: "Váš profil"
instanceInfo: "Informace o instanci"
notifications: "Oznámení"
timeline: "Časová osa"
calendar: "Kalendář"

View File

@ -1302,6 +1302,8 @@ _weekday:
friday: "Freitag"
saturday: "Samstag"
_widgets:
profile: "Profil"
instanceInfo: "Instanzinformationen"
memo: "Merkzettel"
notifications: "Benachrichtigungen"
timeline: "Chronik"

View File

@ -343,6 +343,8 @@ _antennaSources:
userList: "Σημειώματα από καθορισμένη λίστα μελών"
userGroup: "Σημειώματα από μέλη καθορισμένης ομάδας"
_widgets:
profile: "Προφίλ"
instanceInfo: "Πληροφορίες του instance"
notifications: "Ειδοποιήσεις"
timeline: "Χρονολόγιο"
calendar: "Ημερολόγιο"

View File

@ -1302,6 +1302,8 @@ _weekday:
friday: "Friday"
saturday: "Saturday"
_widgets:
profile: "Profile"
instanceInfo: "Instance Information"
memo: "Sticky notes"
notifications: "Notifications"
timeline: "Timeline"

View File

@ -1296,6 +1296,8 @@ _weekday:
friday: "Viernes"
saturday: "Sábado"
_widgets:
profile: "Perfil"
instanceInfo: "información de la instancia"
memo: "Nota adhesiva"
notifications: "Notificaciones"
timeline: "Linea de tiempo"

View File

@ -1289,6 +1289,8 @@ _weekday:
friday: "Vendredi"
saturday: "Samedi"
_widgets:
profile: "Profil"
instanceInfo: "Informations sur linstance"
memo: "Note collante"
notifications: "Notifications"
timeline: "Fil"

View File

@ -1206,6 +1206,8 @@ _weekday:
friday: "Jumat"
saturday: "Sabtu"
_widgets:
profile: "Profil"
instanceInfo: "Informasi Instansi"
memo: "Catatan memo"
notifications: "Pemberitahuan"
timeline: "Linimasa"

View File

@ -1296,6 +1296,8 @@ _weekday:
friday: "Venerdì"
saturday: "Sabato"
_widgets:
profile: "Profilo"
instanceInfo: "Informazioni sull'istanza"
memo: "Promemoria"
notifications: "Notifiche"
timeline: "Timeline"

View File

@ -1335,6 +1335,8 @@ _weekday:
saturday: "土曜日"
_widgets:
profile: "プロフィール"
instanceInfo: "インスタンス情報"
memo: "付箋"
notifications: "通知"
timeline: "タイムライン"

View File

@ -1295,6 +1295,8 @@ _weekday:
friday: "金曜日"
saturday: "土曜日"
_widgets:
profile: "プロフィール"
instanceInfo: "インスタンス情報"
memo: "付箋"
notifications: "通知"
timeline: "タイムライン"

View File

@ -73,6 +73,7 @@ _sfx:
_permissions:
"write:account": "Ẓreg talɣut n umiḍan-ik·im"
_widgets:
profile: "Amaɣnu"
notifications: "Ilɣuyen"
_userList:
chooseList: "Fren tabdart"

View File

@ -69,6 +69,7 @@ _mfm:
_sfx:
notification: "ಅಧಿಸೂಚನೆಗಳು"
_widgets:
profile: "ಪ್ರೊಫೈಲು"
notifications: "ಅಧಿಸೂಚನೆಗಳು"
timeline: "ಸಮಯಸಾಲು"
_cw:

View File

@ -1302,6 +1302,8 @@ _weekday:
friday: "금요일"
saturday: "토요일"
_widgets:
profile: "프로필"
instanceInfo: "인스턴스 정보"
memo: "스티커 메모"
notifications: "알림"
timeline: "타임라인"

View File

@ -440,6 +440,8 @@ _sfx:
notification: "Meldingen"
chat: "Chat"
_widgets:
profile: "Profiel"
instanceInfo: "Serverinformatie"
notifications: "Meldingen"
timeline: "Tijdlijn"
activity: "Activiteit"

View File

@ -1213,6 +1213,8 @@ _weekday:
friday: "Piątek"
saturday: "Sobota"
_widgets:
profile: "Profil"
instanceInfo: "Informacje o instancji"
memo: "Przypięte notatki"
notifications: "Powiadomienia"
timeline: "Oś czasu"

View File

@ -488,6 +488,8 @@ _sfx:
notification: "Notificações"
chat: "Chat"
_widgets:
profile: "Perfil"
instanceInfo: "Informações da instância"
notifications: "Notificações"
timeline: "Timeline"
activity: "atividade"

View File

@ -667,6 +667,8 @@ _sfx:
notification: "Notificări"
chat: "Chat"
_widgets:
profile: "Profil"
instanceInfo: "Informații despre instanță"
notifications: "Notificări"
timeline: "Cronologie"
activity: "Activitate"

View File

@ -1213,6 +1213,8 @@ _weekday:
friday: "Пятница"
saturday: "Суббота"
_widgets:
profile: "Профиль"
instanceInfo: "Информация об инстансе"
memo: "Напоминания"
notifications: "Уведомления"
timeline: "Лента"

View File

@ -1295,6 +1295,8 @@ _weekday:
friday: "Piatok"
saturday: "Sobota"
_widgets:
profile: "Profil"
instanceInfo: "Informácie o serveri"
memo: "Prilepené poznámky"
notifications: "Oznámenia"
timeline: "Časová os"

View File

@ -327,7 +327,16 @@ pinnedNotes: "Fästad not"
enableHcaptcha: "Aktivera hCaptcha"
enableRecaptcha: "Aktivera reCAPTCHA"
enableTurnstile: "Aktivera Turnstile"
antennas: "Antenner"
manageAntennas: "Hantera Antenner"
antennaSource: "Antennkälla"
antennaKeywords: "Nyckelord att lyssna efter"
antennaExcludeKeywords: "Nyckelord att exkludera"
antennaKeywordsDescription: "Separera med mellanslag för en AND kondition, eller med nya linjer för en OR kondition"
notifyAntenna: "Notifiera om nya noter"
withFileAntenna: "Endast noter med filer"
enableServiceworker: "Aktivera pushnotiser i denna webbläsaren"
antennaUsersDescription: "Ange ett användarnamn per linje"
recentlyUpdatedUsers: "Nyligen aktiva användare"
recentlyRegisteredUsers: "Nyligen registrerade användare"
userList: "Listor"
@ -377,7 +386,16 @@ _sfx:
note: "Noter"
notification: "Notifikationer"
chat: "Chatt"
antenna: "Antenner"
_antennaSources:
all: "Alla noter"
homeTimeline: "Noter från följda användare"
users: "Noter från specifika användare"
userList: "Noter från en specificerad lista av användare"
userGroup: "Noter från användare i en specificerad grupp"
_widgets:
profile: "Profil"
instanceInfo: "Instansinformation"
notifications: "Notifikationer"
timeline: "Tidslinje"
activity: "Aktivitet"
@ -395,6 +413,7 @@ _profile:
changeAvatar: "Ändra profilbild"
changeBanner: "Ändra banner"
_exportOrImport:
allNotes: "Alla noter"
followingList: "Följer"
muteList: "Tysta"
blockingList: "Blockera"
@ -423,5 +442,6 @@ _deck:
_columns:
notifications: "Notifikationer"
tl: "Tidslinje"
antenna: "Antenner"
list: "Listor"
mentions: "Omnämningar"

View File

@ -1302,6 +1302,8 @@ _weekday:
friday: "วันศุกร์"
saturday: "วันเสาร์"
_widgets:
profile: "โปรไฟล์"
instanceInfo: "ข้อมูล อินสแตนซ์"
memo: "โน้ตแปะ"
notifications: "การเเจ้งเตือน"
timeline: "ไทม์ไลน์"

View File

@ -53,6 +53,7 @@ _mfm:
_sfx:
notification: "Bildirim"
_widgets:
profile: "Profil"
notifications: "Bildirim"
timeline: "Zaman çizelgesi"
_profile:

View File

@ -1229,6 +1229,8 @@ _weekday:
friday: "П'ятниця"
saturday: "Субота"
_widgets:
profile: "Профіль"
instanceInfo: "Про цей інстанс"
memo: "Нагадування"
notifications: "Сповіщення"
timeline: "Стрічка"

View File

@ -1271,6 +1271,8 @@ _weekday:
friday: "Thứ Sáu"
saturday: "Thứ Bảy"
_widgets:
profile: "Trang cá nhân"
instanceInfo: "Thông tin máy chủ"
memo: "Tút đã ghim"
notifications: "Thông báo"
timeline: "Bảng tin"

View File

@ -922,7 +922,8 @@ numberOfLikes: "点赞数"
show: "显示"
neverShow: "不再显示"
remindMeLater: "稍后提醒我"
didYouLikeMisskey: "你在Misskey玩得还开心吗"
didYouLikeMisskey: "您喜欢Misskey吗"
pleaseDonate: "Misskey是{host}所使用的免费软件。为了今后也能够维持Misskey的开发请在有余力的情况下进行捐助"
_sensitiveMediaDetection:
description: "可以使用机器学习技术自动检测敏感媒体,以便进行审核。服务器负载将略微增加。"
sensitivity: "检测敏感度"
@ -1301,6 +1302,8 @@ _weekday:
friday: "星期五"
saturday: "星期六"
_widgets:
profile: "个人资料"
instanceInfo: "实例信息"
memo: "便签"
notifications: "通知"
timeline: "时间线"
@ -1327,6 +1330,7 @@ _widgets:
userList: "用户列表"
_userList:
chooseList: "选择列表"
clicker: "点击器"
_cw:
hide: "隐藏"
show: "查看更多"

View File

@ -797,7 +797,7 @@ squareAvatars: "頭像以方形顯示"
sent: "發送"
received: "收取"
searchResult: "搜尋結果"
hashtags: "#tag"
hashtags: "標籤"
troubleshooting: "故障排除"
useBlurEffect: "在 UI 上使用模糊效果"
learnMore: "更多資訊"
@ -1159,7 +1159,7 @@ _theme:
navActive: "側邊欄文本 (活動)"
navIndicator: "側邊欄指示符"
link: "鏈接"
hashtag: "#tag"
hashtag: "標籤"
mention: "提到"
mentionMe: "提到了我"
renote: "轉發貼文"
@ -1302,6 +1302,8 @@ _weekday:
friday: "週五"
saturday: "週六"
_widgets:
profile: "個人檔案"
instanceInfo: "實例資訊"
memo: "備忘錄"
notifications: "通知"
timeline: "時間軸"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "13.0.0-beta.36",
"version": "13.0.0-beta.39",
"codename": "indigo",
"repository": {
"type": "git",

View File

@ -0,0 +1,11 @@
export class PollChoiceLength1673336077243 {
name = 'PollChoiceLength1673336077243'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "poll" ALTER COLUMN "choices" TYPE character varying(256) array`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "poll" ALTER COLUMN "choices" TYPE character varying(128) array`);
}
}

View File

@ -24,7 +24,7 @@ export class Poll {
public multiple: boolean;
@Column('varchar', {
length: 128, array: true, default: '{}',
length: 256, array: true, default: '{}',
})
public choices: string[];

View File

@ -8,6 +8,8 @@ import { MAX_NOTE_TEXT_LENGTH } from '@/const.js';
import { Cache } from '@/misc/cache.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js';
import NotesChart from '@/core/chart/charts/notes.js';
import UsersChart from '@/core/chart/charts/users.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
const nodeinfo2_1path = '/nodeinfo/2.1';
@ -27,6 +29,8 @@ export class NodeinfoServerService {
private userEntityService: UserEntityService,
private metaService: MetaService,
private notesChart: NotesChart,
private usersChart: UsersChart,
) {
//this.createServer = this.createServer.bind(this);
}
@ -46,20 +50,27 @@ export class NodeinfoServerService {
public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) {
const nodeinfo2 = async () => {
const now = Date.now();
const notesChart = await this.notesChart.getChart('hour', 1, null);
const localPosts = notesChart.local.total[0];
const usersChart = await this.usersChart.getChart('hour', 1, null);
const total = usersChart.local.total[0];
const [
meta,
total,
activeHalfyear,
activeMonth,
localPosts,
//activeHalfyear,
//activeMonth,
] = await Promise.all([
this.metaService.fetch(true),
this.usersRepository.count({ where: { host: IsNull() } }),
this.usersRepository.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 15552000000)) } }),
this.usersRepository.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 2592000000)) } }),
this.notesRepository.count({ where: { userHost: IsNull() } }),
// 重い
//this.usersRepository.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 15552000000)) } }),
//this.usersRepository.count({ where: { host: IsNull(), lastActiveDate: MoreThan(new Date(now - 2592000000)) } }),
]);
const activeHalfyear = null;
const activeMonth = null;
const proxyAccount = meta.proxyAccountId ? await this.userEntityService.pack(meta.proxyAccountId).catch(() => null) : null;
return {

View File

@ -3,6 +3,8 @@ import { IsNull } from 'typeorm';
import type { InstancesRepository, NoteReactionsRepository, NotesRepository, UsersRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DI } from '@/di-symbols.js';
import NotesChart from '@/core/chart/charts/notes.js';
import UsersChart from '@/core/chart/charts/users.js';
export const meta = {
requireCredential: false,
@ -66,21 +68,24 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
@Inject(DI.noteReactionsRepository)
private noteReactionsRepository: NoteReactionsRepository,
private notesChart: NotesChart,
private usersChart: UsersChart,
) {
super(meta, paramDef, async () => {
const notesChart = await this.notesChart.getChart('hour', 1, null);
const notesCount = notesChart.local.total[0] + notesChart.remote.total[0];
const originalNotesCount = notesChart.local.total[0];
const usersChart = await this.usersChart.getChart('hour', 1, null);
const usersCount = usersChart.local.total[0] + usersChart.remote.total[0];
const originalUsersCount = usersChart.local.total[0];
const [
notesCount,
originalNotesCount,
usersCount,
originalUsersCount,
reactionsCount,
//originalReactionsCount,
instances,
] = await Promise.all([
this.notesRepository.count({ cache: 3600000 }), // 1 hour
this.notesRepository.count({ where: { userHost: IsNull() }, cache: 3600000 }),
this.usersRepository.count({ cache: 3600000 }),
this.usersRepository.count({ where: { host: IsNull() }, cache: 3600000 }),
this.noteReactionsRepository.count({ cache: 3600000 }), // 1 hour
//this.noteReactionsRepository.count({ where: { userHost: IsNull() }, cache: 3600000 }),
this.instancesRepository.count({ cache: 3600000 }),

View File

@ -36,7 +36,6 @@
"insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2",
"json5": "2.2.3",
"katex": "0.16.4",
"matter-js": "0.18.0",
"mfm-js": "0.23.1",
"misskey-js": "0.0.14",
@ -73,7 +72,6 @@
"@types/glob": "8.0.0",
"@types/gulp": "4.0.10",
"@types/gulp-rename": "2.0.1",
"@types/katex": "0.16.0",
"@types/matter-js": "0.18.2",
"@types/punycode": "2.1.0",
"@types/sanitize-html": "^2.8.0",

View File

@ -80,7 +80,6 @@ export default defineComponent({
} else {
if (props.ad && item._shouldInsertAd_) {
return [h(MkAd, {
class: 'a', // advertise()
key: item.id + ':ad',
prefer: ['horizontal', 'horizontal-big'],
}), el];

View File

@ -1,19 +1,19 @@
<template>
<MkModal ref="modal" :prefer-type="'dialog'" :z-priority="'high'" @click="done(true)" @closed="emit('closed')">
<div class="mk-dialog">
<div v-if="icon" class="icon">
<div :class="$style.root">
<div v-if="icon" :class="$style.icon">
<i :class="icon"></i>
</div>
<div v-else-if="!input && !select" class="icon" :class="type">
<i v-if="type === 'success'" class="ti ti-check"></i>
<i v-else-if="type === 'error'" class="ti ti-circle-x"></i>
<i v-else-if="type === 'warning'" class="ti ti-alert-triangle"></i>
<i v-else-if="type === 'info'" class="ti ti-info-circle"></i>
<i v-else-if="type === 'question'" class="ti ti-question-circle"></i>
<MkLoading v-else-if="type === 'waiting'" :em="true"/>
<div v-else-if="!input && !select" :class="[$style.icon, $style['type_' + type]]">
<i v-if="type === 'success'" :class="$style.iconInner" class="ti ti-check"></i>
<i v-else-if="type === 'error'" :class="$style.iconInner" class="ti ti-circle-x"></i>
<i v-else-if="type === 'warning'" :class="$style.iconInner" class="ti ti-alert-triangle"></i>
<i v-else-if="type === 'info'" :class="$style.iconInner" class="ti ti-info-circle"></i>
<i v-else-if="type === 'question'" :class="$style.iconInner" class="ti ti-question-circle"></i>
<MkLoading v-else-if="type === 'waiting'" :class="$style.iconInner" :em="true"/>
</div>
<header v-if="title"><Mfm :text="title"/></header>
<div v-if="text" class="body"><Mfm :text="text"/></div>
<header v-if="title" :class="$style.title"><Mfm :text="title"/></header>
<div v-if="text" :class="$style.text"><Mfm :text="text"/></div>
<MkInput v-if="input" v-model="inputValue" autofocus :type="input.type || 'text'" :placeholder="input.placeholder || undefined" @keydown="onInputKeydown">
<template v-if="input.type === 'password'" #prefix><i class="ti ti-lock"></i></template>
</MkInput>
@ -27,11 +27,11 @@
</optgroup>
</template>
</MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" class="buttons">
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
<MkButton v-if="showOkButton" inline primary :autofocus="!input && !select" @click="ok">{{ (showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt }}</MkButton>
<MkButton v-if="showCancelButton || input || select" inline @click="cancel">{{ i18n.ts.cancel }}</MkButton>
</div>
<div v-if="actions" class="buttons">
<div v-if="actions" :class="$style.buttons">
<MkButton v-for="action in actions" :key="action.text" inline :primary="action.primary" @click="() => { action.callback(); close(); }">{{ action.text }}</MkButton>
</div>
</div>
@ -143,8 +143,8 @@ onBeforeUnmount(() => {
});
</script>
<style lang="scss" scoped>
.mk-dialog {
<style lang="scss" module>
.root {
position: relative;
padding: 32px;
min-width: 320px;
@ -153,56 +153,56 @@ onBeforeUnmount(() => {
text-align: center;
background: var(--panel);
border-radius: var(--radius);
}
> .icon {
font-size: 24px;
.icon {
font-size: 24px;
&.info {
color: #55c4dd;
}
&.success {
color: var(--success);
}
&.error {
color: var(--error);
}
&.warning {
color: var(--warn);
}
> * {
display: block;
margin: 0 auto;
}
& + header {
margin-top: 8px;
}
}
> header {
margin: 0 0 8px 0;
font-weight: bold;
font-size: 1.1em;
& + .body {
margin-top: 8px;
}
}
> .body {
margin: 16px 0 0 0;
}
> .buttons {
margin-top: 16px;
> * {
margin: 0 8px;
}
& + .title {
margin-top: 8px;
}
}
.iconInner {
display: block;
margin: 0 auto;
}
.type_info {
color: #55c4dd;
}
.type_success {
color: var(--success);
}
.type_error {
color: var(--error);
}
.type_warning {
color: var(--warn);
}
.title {
margin: 0 0 8px 0;
font-weight: bold;
font-size: 1.1em;
& + .text {
margin-top: 8px;
}
}
.text {
margin: 16px 0 0 0;
}
.buttons {
margin-top: 16px;
display: flex;
gap: 8px;
flex-wrap: wrap;
justify-content: center;
}
</style>

View File

@ -1,24 +0,0 @@
<template>
<XFormula :formula="formula" :block="block"/>
</template>
<script lang="ts">
import { defineComponent, defineAsyncComponent } from 'vue';
import * as os from '@/os';
export default defineComponent({
components: {
XFormula: defineAsyncComponent(() => import('@/components/MkFormulaCore.vue')),
},
props: {
formula: {
type: String,
required: true,
},
block: {
type: Boolean,
required: true,
},
},
});
</script>

View File

@ -1,34 +0,0 @@
<!-- eslint-disable vue/no-v-html -->
<template>
<div v-if="block" v-html="compiledFormula"></div>
<span v-else v-html="compiledFormula"></span>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import katex from 'katex';
export default defineComponent({
props: {
formula: {
type: String,
required: true,
},
block: {
type: Boolean,
required: true,
},
},
computed: {
compiledFormula(): any {
return katex.renderToString(this.formula, {
throwOnError: false,
} as any);
},
},
});
</script>
<style>
@import "../../node_modules/katex/dist/katex.min.css";
</style>

View File

@ -75,7 +75,7 @@ function close() {
&.asDrawer {
width: 100%;
padding: 16px 16px calc(env(safe-area-inset-bottom, 0px) + 16px) 16px;
padding: 16px 16px max(env(safe-area-inset-bottom, 0px), 16px) 16px;
border-radius: 24px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;

View File

@ -1,10 +1,10 @@
<template>
<component
:is="self ? 'MkA' : 'a'" ref="el" class="xlcxczvw _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
:is="self ? 'MkA' : 'a'" ref="el" style="word-break: break-all;" class="_link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
:title="url"
>
<slot></slot>
<i v-if="target === '_blank'" class="ti ti-external-link icon"></i>
<i v-if="target === '_blank'" class="ti ti-external-link" :class="$style.icon"></i>
</component>
</template>
@ -35,13 +35,9 @@ useTooltip($$(el), (showing) => {
});
</script>
<style lang="scss" scoped>
.xlcxczvw {
word-break: break-all;
> .icon {
padding-left: 2px;
font-size: .9em;
}
<style lang="scss" module>
.icon {
padding-left: 2px;
font-size: .9em;
}
</style>

View File

@ -2,54 +2,54 @@
<div>
<div
ref="itemsEl" v-hotkey="keymap"
class="rrevdjwt _popup _shadow"
:class="{ center: align === 'center', asDrawer }"
class="_popup _shadow"
:class="[$style.root, { [$style.center]: align === 'center', [$style.asDrawer]: 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>
<span v-else-if="item.type === 'label'" class="label item">
<div v-if="item === null" :class="$style.divider"></div>
<span v-else-if="item.type === 'label'" :class="[$style.label, $style.item]">
<span>{{ item.text }}</span>
</span>
<span v-else-if="item.type === 'pending'" :tabindex="i" class="pending item">
<span v-else-if="item.type === 'pending'" :tabindex="i" :class="[$style.pending, $style.item]">
<span><MkEllipsis/></span>
</span>
<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="item.icon"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
<span>{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
</MkA>
<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="item.icon"></i>
<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<span>{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
</a>
<button v-else-if="item.type === 'user'" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkAvatar :user="item.user" class="avatar"/><MkUserName :user="item.user"/>
<span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
<button v-else-if="item.type === 'user'" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
</button>
<span v-else-if="item.type === 'switch'" :tabindex="i" class="item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<span v-else-if="item.type === 'switch'" :tabindex="i" :class="$style.item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</MkSwitch>
</span>
<button v-else-if="item.type === 'parent'" :tabindex="i" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)">
<i v-if="item.icon" class="ti-fw" :class="item.icon"></i>
<button v-else-if="item.type === 'parent'" :tabindex="i" class="_button" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="showChildren(item, $event)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<span>{{ item.text }}</span>
<span class="caret"><i class="ti ti-caret-right ti-fw"></i></span>
<span :class="$style.caret"><i class="ti ti-caret-right ti-fw"></i></span>
</button>
<button v-else :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="item.icon"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
<button v-else :tabindex="i" class="_button" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
<span>{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
</button>
</template>
<span v-if="items2.length === 0" class="none item">
<span v-if="items2.length === 0" :class="[$style.none, $style.item]">
<span>{{ i18n.ts.none }}</span>
</span>
</div>
<div v-if="childMenu" class="child">
<div v-if="childMenu" :class="$style.child">
<XChild ref="child" :items="childMenu" :target-element="childTarget" :root-element="itemsEl" showing @actioned="childActioned"/>
</div>
</div>
@ -186,8 +186,8 @@ onBeforeUnmount(() => {
});
</script>
<style lang="scss" scoped>
.rrevdjwt {
<style lang="scss" module>
.root {
padding: 8px 0;
box-sizing: border-box;
min-width: 200px;
@ -200,143 +200,8 @@ onBeforeUnmount(() => {
}
}
> .item {
display: block;
position: relative;
padding: 5px 16px;
width: 100%;
box-sizing: border-box;
white-space: nowrap;
font-size: 0.9em;
line-height: 20px;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
&:before {
content: "";
display: block;
position: absolute;
z-index: -1;
top: 0;
left: 0;
right: 0;
margin: auto;
width: calc(100% - 16px);
height: 100%;
border-radius: 6px;
}
&:not(:disabled):hover {
color: var(--accent);
text-decoration: none;
&:before {
background: var(--accentedBg);
}
}
&.danger {
color: #ff2a2a;
&:hover {
color: #fff;
&:before {
background: #ff4242;
}
}
&:active {
color: #fff;
&:before {
background: #d42e2e !important;
}
}
}
&:active,
&.active {
color: var(--fgOnAccent) !important;
opacity: 1;
&:before {
background: var(--accent) !important;
}
}
&:not(:active):focus-visible {
box-shadow: 0 0 0 2px var(--focus) inset;
}
&.label {
pointer-events: none;
font-size: 0.7em;
padding-bottom: 4px;
> span {
opacity: 0.7;
}
}
&.pending {
pointer-events: none;
opacity: 0.7;
}
&.none {
pointer-events: none;
opacity: 0.7;
}
&.parent {
display: flex;
align-items: center;
cursor: default;
> .caret {
margin-left: auto;
}
&.childShowing {
color: var(--accent);
text-decoration: none;
&:before {
background: var(--accentedBg);
}
}
}
> i {
margin-right: 5px;
width: 20px;
}
> .avatar {
margin-right: 5px;
width: 20px;
height: 20px;
}
> .indicator {
position: absolute;
top: 5px;
left: 13px;
color: var(--indicator);
font-size: 12px;
animation: blink 1s infinite;
}
}
> .divider {
margin: 8px 0;
border-top: solid 0.5px var(--divider);
}
&.asDrawer {
padding: 12px 0 calc(env(safe-area-inset-bottom, 0px) + 12px) 0;
padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0;
width: 100%;
border-radius: 24px;
border-bottom-right-radius: 0;
@ -351,7 +216,7 @@ onBeforeUnmount(() => {
border-radius: 12px;
}
> i {
> .icon {
margin-right: 14px;
width: 24px;
}
@ -362,4 +227,139 @@ onBeforeUnmount(() => {
}
}
}
.item {
display: block;
position: relative;
padding: 5px 16px;
width: 100%;
box-sizing: border-box;
white-space: nowrap;
font-size: 0.9em;
line-height: 20px;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
&:before {
content: "";
display: block;
position: absolute;
z-index: -1;
top: 0;
left: 0;
right: 0;
margin: auto;
width: calc(100% - 16px);
height: 100%;
border-radius: 6px;
}
&:not(:disabled):hover {
color: var(--accent);
text-decoration: none;
&:before {
background: var(--accentedBg);
}
}
&.danger {
color: #ff2a2a;
&:hover {
color: #fff;
&:before {
background: #ff4242;
}
}
&:active {
color: #fff;
&:before {
background: #d42e2e !important;
}
}
}
&:active,
&.active {
color: var(--fgOnAccent) !important;
opacity: 1;
&:before {
background: var(--accent) !important;
}
}
&:not(:active):focus-visible {
box-shadow: 0 0 0 2px var(--focus) inset;
}
&.label {
pointer-events: none;
font-size: 0.7em;
padding-bottom: 4px;
> span {
opacity: 0.7;
}
}
&.pending {
pointer-events: none;
opacity: 0.7;
}
&.none {
pointer-events: none;
opacity: 0.7;
}
&.parent {
display: flex;
align-items: center;
cursor: default;
&.childShowing {
color: var(--accent);
text-decoration: none;
&:before {
background: var(--accentedBg);
}
}
}
}
.icon {
margin-right: 5px;
width: 20px;
}
.caret {
margin-left: auto;
}
.avatar {
margin-right: 5px;
width: 20px;
height: 20px;
}
.indicator {
position: absolute;
top: 5px;
left: 13px;
color: var(--indicator);
font-size: 12px;
animation: blink 1s infinite;
}
.divider {
margin: 8px 0;
border-top: solid 0.5px var(--divider);
}
</style>

View File

@ -4,27 +4,26 @@
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
class="tkcbzcuz"
:class="$style.root"
:tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }"
>
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
<div v-if="pinned" class="info"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
<div v-if="appearNote._prId_" class="info"><i class="fas fa-bullhorn"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>
<div v-if="appearNote._featuredId_" class="info"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>
<div v-if="isRenote" class="renote">
<MkAvatar v-once class="avatar" :user="note.user"/>
<i class="ti ti-repeat"></i>
<I18n :src="i18n.ts.renotedBy" tag="span">
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo"/>
<div v-if="pinned" :class="$style.tip"><i class="ti ti-pin"></i> {{ i18n.ts.pinnedNote }}</div>
<!--<div v-if="appearNote._prId_" class="tip"><i class="fas fa-bullhorn"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ti ti-x"></i></button></div>-->
<!--<div v-if="appearNote._featuredId_" class="tip"><i class="ti ti-bolt"></i> {{ i18n.ts.featured }}</div>-->
<div v-if="isRenote" :class="$style.renote">
<MkAvatar v-once :class="$style.renoteAvatar" :user="note.user"/>
<i class="ti ti-repeat" style="margin-right: 4px;"></i>
<I18n :src="i18n.ts.renotedBy" tag="span" :class="$style.renoteText">
<template #user>
<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)">
<MkA v-user-preview="note.userId" :class="$style.renoteUserName" :to="userPage(note.user)">
<MkUserName :user="note.user"/>
</MkA>
</template>
</I18n>
<div class="info">
<button ref="renoteTime" class="_button time" @click="showRenoteMenu()">
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
<div :class="$style.renoteInfo">
<button ref="renoteTime" :class="$style.renoteTime" class="_button" @click="showRenoteMenu()">
<i v-if="isMyRenote" class="ti ti-dots" :class="$style.renoteMenu"></i>
<MkTime :time="note.createdAt"/>
</button>
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
@ -35,80 +34,80 @@
<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
</div>
</div>
<article class="article" @contextmenu.stop="onContextmenu">
<MkAvatar v-once class="avatar" :user="appearNote.user"/>
<div class="main">
<MkNoteHeader class="header" :note="appearNote" :mini="true"/>
<MkInstanceTicker v-if="showTicker" class="ticker" :instance="appearNote.user.instance"/>
<div class="body">
<p v-if="appearNote.cw != null" class="cw">
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i"/>
<article :class="$style.article" @contextmenu.stop="onContextmenu">
<MkAvatar v-once :class="$style.avatar" :user="appearNote.user"/>
<div :class="$style.main">
<MkNoteHeader :class="$style.header" :note="appearNote" :mini="true"/>
<MkInstanceTicker v-if="showTicker" :class="$style.ticker" :instance="appearNote.user.instance"/>
<div style="container-type: inline-size;">
<p v-if="appearNote.cw != null" :class="$style.cw">
<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :i="$i"/>
<MkCwButton v-model="showContent" :note="appearNote"/>
</p>
<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed, isLong }">
<div class="text">
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
<div :class="$style.text">
<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
<MkA v-if="appearNote.replyId" class="reply" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
<Mfm v-if="appearNote.text" v-once :text="appearNote.text" :author="appearNote.user" :i="$i"/>
<a v-if="appearNote.renote != null" class="rp">RN:</a>
<div v-if="translating || translation" class="translation">
<div v-if="translating || translation" :class="$style.translation">
<MkLoading v-if="translating" mini/>
<div v-else class="translated">
<div v-else :class="$style.translated">
<b>{{ $t('translatedFrom', { x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :i="$i"/>
</div>
</div>
</div>
<div v-if="appearNote.files.length > 0" class="files">
<div v-if="appearNote.files.length > 0" :class="$style.files">
<MkMediaList :media-list="appearNote.files"/>
</div>
<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
<div v-if="appearNote.renote" class="renote"><MkNoteSimple :note="appearNote.renote" class="note"/></div>
<button v-if="isLong && collapsed" class="fade _button" @click="collapsed = false">
<span>{{ i18n.ts.showMore }}</span>
<MkPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" :class="$style.poll"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" :class="$style.urlPreview"/>
<div v-if="appearNote.renote" :class="$style.quote"><MkNoteSimple :note="appearNote.renote" :class="$style.quoteNote"/></div>
<button v-if="isLong && collapsed" :class="$style.collapsed" class="_button" @click="collapsed = false">
<span :class="$style.collapsedLabel">{{ i18n.ts.showMore }}</span>
</button>
<button v-else-if="isLong && !collapsed" class="showLess _button" @click="collapsed = true">
<span>{{ i18n.ts.showLess }}</span>
<button v-else-if="isLong && !collapsed" :class="$style.showLess" class="_button" @click="collapsed = true">
<span :class="$style.showLessLabel">{{ i18n.ts.showLess }}</span>
</button>
</div>
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
<MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA>
</div>
<footer class="footer">
<footer :class="$style.footer">
<MkReactionsViewer ref="reactionsViewer" :note="appearNote"/>
<button class="button _button" @click="reply()">
<button :class="$style.footerButton" class="_button" @click="reply()">
<i class="ti ti-arrow-back-up"></i>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
<p v-if="appearNote.repliesCount > 0" :class="$style.footerButtonCount">{{ appearNote.repliesCount }}</p>
</button>
<button
v-if="canRenote"
ref="renoteButton"
class="button _button"
:class="$style.footerButton"
class="_button"
@mousedown="renote()"
>
<i class="ti ti-repeat"></i>
<p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p>
<p v-if="appearNote.renoteCount > 0" :class="$style.footerButtonCount">{{ appearNote.renoteCount }}</p>
</button>
<button v-else class="button _button" disabled>
<button v-else :class="$style.footerButton" class="_button" disabled>
<i class="ti ti-ban"></i>
</button>
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @mousedown="react()">
<button v-if="appearNote.myReaction == null" ref="reactButton" :class="$style.footerButton" class="_button" @mousedown="react()">
<i class="ti ti-plus"></i>
</button>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<button v-if="appearNote.myReaction != null" ref="reactButton" :class="$style.footerButton" class="_button" @click="undoReact(appearNote)">
<i class="ti ti-minus"></i>
</button>
<button ref="menuButton" class="button _button" @mousedown="menu()">
<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown="menu()">
<i class="ti ti-dots"></i>
</button>
</footer>
</div>
</article>
</div>
<div v-else class="muted" @click="muted = false">
<div v-else :class="$style.muted" @click="muted = false">
<I18n :src="i18n.ts.userSaysSomething" tag="small">
<template #name>
<MkA v-user-preview="appearNote.userId" class="name" :to="userPage(appearNote.user)">
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
<MkUserName :user="appearNote.user"/>
</MkA>
</template>
@ -349,8 +348,8 @@ function readPromo() {
}
</script>
<style lang="scss" scoped>
.tkcbzcuz {
<style lang="scss" module>
.root {
position: relative;
transition: box-shadow 0.1s ease;
font-size: 1.05em;
@ -387,322 +386,259 @@ function readPromo() {
}
}
&:hover > .article > .main > .footer > .button {
&:hover > .article > .main > .footer > .footerButton {
opacity: 1;
}
> .info {
display: flex;
align-items: center;
padding: 16px 32px 8px 32px;
line-height: 24px;
font-size: 90%;
white-space: pre;
color: #d28a3f;
> i {
margin-right: 4px;
}
> .hide {
margin-left: auto;
color: inherit;
}
}
> .info + .article {
padding-top: 8px;
}
> .reply-to {
opacity: 0.7;
padding-bottom: 0;
}
> .renote {
display: flex;
align-items: center;
padding: 16px 32px 8px 32px;
line-height: 28px;
white-space: pre;
color: var(--renote);
> .avatar {
flex-shrink: 0;
display: inline-block;
width: 28px;
height: 28px;
margin: 0 8px 0 0;
border-radius: 6px;
}
> i {
margin-right: 4px;
}
> span {
overflow: hidden;
flex-shrink: 1;
text-overflow: ellipsis;
white-space: nowrap;
> .name {
font-weight: bold;
}
}
> .info {
margin-left: auto;
font-size: 0.9em;
> .time {
flex-shrink: 0;
color: inherit;
> .dropdownIcon {
margin-right: 4px;
}
}
}
}
> .renote + .article {
padding-top: 8px;
}
> .article {
display: flex;
padding: 28px 32px 18px;
> .avatar {
flex-shrink: 0;
display: block;
margin: 0 14px 8px 0;
width: 58px;
height: 58px;
position: sticky;
top: calc(22px + var(--stickyTop, 0px));
left: 0;
}
> .main {
flex: 1;
min-width: 0;
> .body {
container-type: inline-size;
> .cw {
cursor: default;
display: block;
margin: 0;
padding: 0;
overflow-wrap: break-word;
> .text {
margin-right: 8px;
}
}
> .content {
&.isLong {
> .showLess {
width: 100%;
margin-top: 1em;
position: sticky;
bottom: 1em;
> span {
display: inline-block;
background: var(--popup);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
}
}
}
&.collapsed {
position: relative;
max-height: 9em;
overflow: clip;
> .fade {
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
> span {
display: inline-block;
background: var(--panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
}
&:hover {
> span {
background: var(--panelHighlight);
}
}
}
}
> .text {
overflow-wrap: break-word;
> .reply {
color: var(--accent);
margin-right: 0.5em;
}
> .rp {
margin-left: 4px;
font-style: oblique;
color: var(--renote);
}
> .translation {
border: solid 0.5px var(--divider);
border-radius: var(--radius);
padding: 12px;
margin-top: 8px;
}
}
> .url-preview {
margin-top: 8px;
}
> .poll {
font-size: 80%;
}
> .renote {
padding: 8px 0;
> .note {
padding: 16px;
border: dashed 1px var(--renote);
border-radius: 8px;
}
}
}
> .channel {
opacity: 0.7;
font-size: 80%;
}
}
> .footer {
> .button {
margin: 0;
padding: 8px;
opacity: 0.7;
&:not(:last-child) {
margin-right: 28px;
}
&:hover {
color: var(--fgHighlighted);
}
> .count {
display: inline;
margin: 0 0 0 8px;
opacity: 0.7;
}
&.reacted {
color: var(--accent);
}
}
}
}
}
> .reply {
border-top: solid 0.5px var(--divider);
}
}
@container (max-width: 500px) {
.tkcbzcuz {
font-size: 0.9em;
.tip {
display: flex;
align-items: center;
padding: 16px 32px 8px 32px;
line-height: 24px;
font-size: 90%;
white-space: pre;
color: #d28a3f;
}
> .article {
> .avatar {
width: 50px;
height: 50px;
}
}
.tip + .article {
padding-top: 8px;
}
.replyTo {
opacity: 0.7;
padding-bottom: 0;
}
.renote {
display: flex;
align-items: center;
padding: 16px 32px 8px 32px;
line-height: 28px;
white-space: pre;
color: var(--renote);
& + .article {
padding-top: 8px;
}
}
.renoteAvatar {
flex-shrink: 0;
display: inline-block;
width: 28px;
height: 28px;
margin: 0 8px 0 0;
border-radius: 6px;
}
.renoteText {
overflow: hidden;
flex-shrink: 1;
text-overflow: ellipsis;
white-space: nowrap;
}
.renoteUserName {
font-weight: bold;
}
.renoteInfo {
margin-left: auto;
font-size: 0.9em;
}
.renoteTime {
flex-shrink: 0;
color: inherit;
}
.renoteMenu {
margin-right: 4px;
}
.article {
display: flex;
padding: 28px 32px 18px;
}
.avatar {
flex-shrink: 0;
display: block !important;
margin: 0 14px 8px 0;
width: 58px;
height: 58px;
position: sticky !important;
top: calc(22px + var(--stickyTop, 0px));
left: 0;
}
.main {
flex: 1;
min-width: 0;
}
.cw {
cursor: default;
display: block;
margin: 0;
padding: 0;
overflow-wrap: break-word;
}
.showLess {
width: 100%;
margin-top: 1em;
position: sticky;
bottom: 1em;
}
.howLessLabel {
display: inline-block;
background: var(--popup);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
}
.contentCollapsed {
position: relative;
max-height: 9em;
overflow: clip;
}
.collapsed {
display: block;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 64px;
background: linear-gradient(0deg, var(--panel), var(--X15));
&:hover > .collapsedLabel {
background: var(--panelHighlight);
}
}
.collapsedLabel {
display: inline-block;
background: var(--panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
box-shadow: 0 2px 6px rgb(0 0 0 / 20%);
}
.text {
overflow-wrap: break-word;
}
.replyIcon {
color: var(--accent);
margin-right: 0.5em;
}
.translation {
border: solid 0.5px var(--divider);
border-radius: var(--radius);
padding: 12px;
margin-top: 8px;
}
.urlPreview {
margin-top: 8px;
}
.poll {
font-size: 80%;
}
.quote {
padding: 8px 0;
}
.quoteNote {
padding: 16px;
border: dashed 1px var(--renote);
border-radius: 8px;
}
.channel {
opacity: 0.7;
font-size: 80%;
}
.footerButton {
margin: 0;
padding: 8px;
opacity: 0.7;
&:not(:last-child) {
margin-right: 28px;
}
&:hover {
color: var(--fgHighlighted);
}
}
.footerButtonCount {
display: inline;
margin: 0 0 0 8px;
opacity: 0.7;
}
@container (max-width: 500px) {
.root {
font-size: 0.9em;
}
.avatar {
width: 50px;
height: 50px;
}
}
@container (max-width: 450px) {
.tkcbzcuz {
> .renote {
padding: 8px 16px 0 16px;
}
.renote {
padding: 8px 16px 0 16px;
}
> .info {
padding: 8px 16px 0 16px;
}
.tip {
padding: 8px 16px 0 16px;
}
> .article {
padding: 14px 16px 9px;
.article {
padding: 14px 16px 9px;
}
> .avatar {
margin: 0 10px 8px 0;
width: 46px;
height: 46px;
top: calc(14px + var(--stickyTop, 0px));
}
}
.avatar {
margin: 0 10px 8px 0;
width: 46px;
height: 46px;
top: calc(14px + var(--stickyTop, 0px));
}
}
@container (max-width: 350px) {
.tkcbzcuz {
> .article {
> .main {
> .footer {
> .button {
&:not(:last-child) {
margin-right: 18px;
}
}
}
}
.footerButton {
&:not(:last-child) {
margin-right: 18px;
}
}
}
@container (max-width: 300px) {
.tkcbzcuz {
> .article {
> .avatar {
width: 44px;
height: 44px;
}
.avatar {
width: 44px;
height: 44px;
}
> .main {
> .footer {
> .button {
&:not(:last-child) {
margin-right: 12px;
}
}
}
}
.footerButton {
&:not(:last-child) {
margin-right: 12px;
}
}
}

View File

@ -1,12 +1,12 @@
<template>
<header class="kkwtjztg">
<MkA v-once v-user-preview="note.user.id" class="name" :to="userPage(note.user)">
<header :class="$style.root">
<MkA v-once v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
<MkUserName :user="note.user"/>
</MkA>
<div v-if="note.user.isBot" class="is-bot">bot</div>
<div class="username"><MkAcct :user="note.user"/></div>
<div class="info">
<MkA class="created-at" :to="notePage(note)">
<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
<div :class="$style.username"><MkAcct :user="note.user"/></div>
<div :class="$style.info">
<MkA :to="notePage(note)">
<MkTime :time="note.createdAt"/>
</MkA>
<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
@ -32,49 +32,49 @@ defineProps<{
}>();
</script>
<style lang="scss" scoped>
.kkwtjztg {
<style lang="scss" module>
.root {
display: flex;
align-items: baseline;
white-space: nowrap;
}
> .name {
flex-shrink: 1;
display: block;
margin: 0 .5em 0 0;
padding: 0;
overflow: hidden;
font-size: 1em;
font-weight: bold;
text-decoration: none;
text-overflow: ellipsis;
.name {
flex-shrink: 1;
display: block;
margin: 0 .5em 0 0;
padding: 0;
overflow: hidden;
font-size: 1em;
font-weight: bold;
text-decoration: none;
text-overflow: ellipsis;
&:hover {
text-decoration: underline;
}
}
> .is-bot {
flex-shrink: 0;
align-self: center;
margin: 0 .5em 0 0;
padding: 1px 6px;
font-size: 80%;
border: solid 0.5px var(--divider);
border-radius: 3px;
}
> .username {
flex-shrink: 9999999;
margin: 0 .5em 0 0;
overflow: hidden;
text-overflow: ellipsis;
}
> .info {
flex-shrink: 0;
margin-left: auto;
font-size: 0.9em;
&:hover {
text-decoration: underline;
}
}
.isBot {
flex-shrink: 0;
align-self: center;
margin: 0 .5em 0 0;
padding: 1px 6px;
font-size: 80%;
border: solid 0.5px var(--divider);
border-radius: 3px;
}
.username {
flex-shrink: 9999999;
margin: 0 .5em 0 0;
overflow: hidden;
text-overflow: ellipsis;
}
.info {
flex-shrink: 0;
margin-left: auto;
font-size: 0.9em;
}
</style>

View File

@ -1,15 +1,15 @@
<template>
<div class="yohlumlk">
<MkAvatar class="avatar" :user="note.user"/>
<div class="main">
<MkNoteHeader 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"/>
<div :class="$style.root">
<MkAvatar :class="$style.avatar" :user="note.user"/>
<div :class="$style.main">
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
<div>
<p v-if="note.cw != null" :class="$style.cw">
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :i="$i"/>
<MkCwButton v-model="showContent" :note="note"/>
</p>
<div v-show="note.cw == null || showContent" class="content">
<MkSubNoteContent class="text" :note="note"/>
<div v-show="note.cw == null || showContent">
<MkSubNoteContent :class="$style.text" :note="note"/>
</div>
</div>
</div>
@ -31,73 +31,60 @@ const props = defineProps<{
const showContent = $ref(false);
</script>
<style lang="scss" scoped>
.yohlumlk {
<style lang="scss" module>
.root {
display: flex;
margin: 0;
padding: 0;
overflow: clip;
font-size: 0.95em;
}
> .avatar {
flex-shrink: 0;
display: block;
margin: 0 10px 0 0;
width: 40px;
height: 40px;
border-radius: 8px;
}
.avatar {
flex-shrink: 0;
display: block;
margin: 0 10px 0 0;
width: 40px;
height: 40px;
border-radius: 8px;
}
> .main {
flex: 1;
min-width: 0;
.main {
flex: 1;
min-width: 0;
}
> .header {
margin-bottom: 2px;
}
.header {
margin-bottom: 2px;
}
> .body {
.cw {
cursor: default;
display: block;
margin: 0;
padding: 0;
overflow-wrap: break-word;
}
> .cw {
cursor: default;
display: block;
margin: 0;
padding: 0;
overflow-wrap: break-word;
> .text {
margin-right: 8px;
}
}
> .content {
> .text {
cursor: default;
margin: 0;
padding: 0;
}
}
}
}
.text {
cursor: default;
margin: 0;
padding: 0;
}
@container (min-width: 350px) {
.yohlumlk {
> .avatar {
margin: 0 10px 0 0;
width: 44px;
height: 44px;
}
.avatar {
margin: 0 10px 0 0;
width: 44px;
height: 44px;
}
}
@container (min-width: 500px) {
.yohlumlk {
> .avatar {
margin: 0 12px 0 0;
width: 48px;
height: 48px;
}
.avatar {
margin: 0 12px 0 0;
width: 48px;
height: 48px;
}
}
</style>

View File

@ -1,25 +1,25 @@
<template>
<div class="wrpstxzv" :class="{ children: depth > 1 }">
<div class="main">
<MkAvatar class="avatar" :user="note.user"/>
<div class="body">
<MkNoteHeader 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"/>
<div :class="[$style.root, { [$style.children]: depth > 1 }]">
<div :class="$style.main">
<MkAvatar :class="$style.avatar" :user="note.user"/>
<div :class="$style.body">
<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
<div>
<p v-if="note.cw != null" :class="$style.cw">
<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :i="$i"/>
<MkCwButton v-model="showContent" :note="note"/>
</p>
<div v-show="note.cw == null || showContent" class="content">
<MkSubNoteContent class="text" :note="note"/>
<div v-show="note.cw == null || showContent">
<MkSubNoteContent :class="$style.text" :note="note"/>
</div>
</div>
</div>
</div>
<template v-if="depth < 5">
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :detail="true" :depth="depth + 1"/>
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="$style.reply" :detail="true" :depth="depth + 1"/>
</template>
<div v-else class="more">
<MkA class="text _link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ti ti-chevron-double-right"></i></MkA>
<div v-else :class="$style.more">
<MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ti ti-chevron-double-right"></i></MkA>
</div>
</div>
</template>
@ -57,8 +57,8 @@ if (props.detail) {
}
</script>
<style lang="scss" scoped>
.wrpstxzv {
<style lang="scss" module>
.root {
padding: 16px 32px;
font-size: 0.9em;
@ -66,62 +66,54 @@ if (props.detail) {
padding: 10px 0 0 16px;
font-size: 1em;
}
}
> .main {
display: flex;
.main {
display: flex;
}
> .avatar {
flex-shrink: 0;
display: block;
margin: 0 8px 0 0;
width: 38px;
height: 38px;
border-radius: 8px;
}
.avatar {
flex-shrink: 0;
display: block;
margin: 0 8px 0 0;
width: 38px;
height: 38px;
border-radius: 8px;
}
> .body {
flex: 1;
min-width: 0;
.body {
flex: 1;
min-width: 0;
}
> .header {
margin-bottom: 2px;
}
.header {
margin-bottom: 2px;
}
> .body {
> .cw {
cursor: default;
display: block;
margin: 0;
padding: 0;
overflow-wrap: break-word;
.cw {
cursor: default;
display: block;
margin: 0;
padding: 0;
overflow-wrap: break-word;
}
> .text {
margin-right: 8px;
}
}
.text {
margin: 0;
padding: 0;
}
> .content {
> .text {
margin: 0;
padding: 0;
}
}
}
}
}
.reply, .more {
border-left: solid 0.5px var(--divider);
margin-top: 10px;
}
> .reply, > .more {
border-left: solid 0.5px var(--divider);
margin-top: 10px;
}
> .more {
padding: 10px 0 0 16px;
}
.more {
padding: 10px 0 0 16px;
}
@container (max-width: 450px) {
.wrpstxzv {
.root {
padding: 14px 16px;
&.children {

View File

@ -8,10 +8,10 @@
</template>
<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" :ad="true" class="notes">
<XNote :key="note._featuredId_ || note._prId_ || note.id" class="qtqtichx" :note="note"/>
</XList>
<div :class="[$style.root, { [$style.noGap]: noGap }]">
<MkDateSeparatedList ref="notes" v-slot="{ item: note }" :items="notes" :direction="pagination.reversed ? 'up' : 'down'" :reversed="pagination.reversed" :no-gap="noGap" :ad="true" :class="$style.notes">
<XNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
</MkDateSeparatedList>
</div>
</template>
</MkPagination>
@ -20,7 +20,7 @@
<script lang="ts" setup>
import { shallowRef } from 'vue';
import XNote from '@/components/MkNote.vue';
import XList from '@/components/MkDateSeparatedList.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue';
import { i18n } from '@/i18n';
@ -36,8 +36,8 @@ defineExpose({
});
</script>
<style lang="scss" scoped>
.giivymft {
<style lang="scss" module>
.root {
&.noGap {
> .notes {
background: var(--panel);
@ -48,7 +48,7 @@ defineExpose({
> .notes {
background: var(--bg);
.qtqtichx {
.note {
background: var(--panel);
border-radius: var(--radius);
}

View File

@ -1,10 +1,10 @@
<template>
<div ref="elRef" class="qglefbjs" :class="notification.type">
<div v-once class="head">
<MkAvatar v-if="notification.type === 'pollEnded'" class="icon" :user="notification.note.user"/>
<MkAvatar v-else-if="notification.user" class="icon" :user="notification.user"/>
<img v-else-if="notification.icon" class="icon" :src="notification.icon" alt=""/>
<div class="sub-icon" :class="notification.type">
<div ref="elRef" :class="$style.root">
<div v-once :class="$style.head">
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user"/>
<MkAvatar v-else-if="notification.user" :class="$style.icon" :user="notification.user"/>
<img v-else-if="notification.icon" :class="$style.icon" :src="notification.icon" alt=""/>
<div :class="[$style.subIcon, $style['t_' + notification.type]]">
<i v-if="notification.type === 'follow'" class="ti ti-plus"></i>
<i v-else-if="notification.type === 'receiveFollowRequest'" class="ti ti-clock"></i>
<i v-else-if="notification.type === 'followRequestAccepted'" class="ti ti-check"></i>
@ -21,46 +21,47 @@
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
:custom-emojis="notification.note.emojis"
:no-style="true"
style="width: 100%; height: 100%;"
/>
</div>
</div>
<div class="tail">
<header>
<div :class="$style.tail">
<header :class="$style.header">
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" class="name" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
<span v-else>{{ notification.header }}</span>
<MkTime v-if="withTime" :time="notification.createdAt" class="time"/>
<MkTime v-if="withTime" :time="notification.createdAt" :class="$style.headerTime"/>
</header>
<div v-once class="content">
<MkA v-if="notification.type === 'reaction'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote"></i>
<div v-once :class="$style.content">
<MkA v-if="notification.type === 'reaction'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote" :class="$style.quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
<i class="ti ti-quote"></i>
<i class="ti ti-quote" :class="$style.quote"></i>
</MkA>
<MkA v-else-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
<i class="ti ti-quote"></i>
<MkA v-else-if="notification.type === 'renote'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
<i class="ti ti-quote" :class="$style.quote"></i>
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/>
<i class="ti ti-quote"></i>
<i class="ti ti-quote" :class="$style.quote"></i>
</MkA>
<MkA v-else-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<MkA v-else-if="notification.type === 'reply'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
</MkA>
<MkA v-else-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<MkA v-else-if="notification.type === 'mention'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
</MkA>
<MkA v-else-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<MkA v-else-if="notification.type === 'quote'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
</MkA>
<MkA v-else-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote"></i>
<MkA v-else-if="notification.type === 'pollEnded'" :class="$style.text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
<i class="ti ti-quote" :class="$style.quote"></i>
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
<i class="ti ti-quote"></i>
<i class="ti ti-quote" :class="$style.quote"></i>
</MkA>
<span v-else-if="notification.type === 'follow'" class="text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
<span v-else-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
<span v-else-if="notification.type === 'receiveFollowRequest'" class="text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span>
<span v-else-if="notification.type === 'groupInvited'" class="text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span>
<span v-else-if="notification.type === 'app'" class="text">
<span v-else-if="notification.type === 'follow'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.youGotNewFollower }}<div v-if="full"><MkFollowButton :user="notification.user" :full="true"/></div></span>
<span v-else-if="notification.type === 'followRequestAccepted'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
<span v-else-if="notification.type === 'receiveFollowRequest'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.receiveFollowRequest }}<div v-if="full && !followRequestDone"><button class="_textButton" @click="acceptFollowRequest()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectFollowRequest()">{{ i18n.ts.reject }}</button></div></span>
<span v-else-if="notification.type === 'groupInvited'" :class="$style.text" style="opacity: 0.6;">{{ i18n.ts.groupInvited }}: <b>{{ notification.invitation.group.name }}</b><div v-if="full && !groupInviteDone"><button class="_textButton" @click="acceptGroupInvitation()">{{ i18n.ts.accept }}</button> | <button class="_textButton" @click="rejectGroupInvitation()">{{ i18n.ts.reject }}</button></div></span>
<span v-else-if="notification.type === 'app'" :class="$style.text">
<Mfm :text="notification.body" :nowrap="!full"/>
</span>
</div>
@ -156,8 +157,8 @@ useTooltip(reactionRef, (showing) => {
});
</script>
<style lang="scss" scoped>
.qglefbjs {
<style lang="scss" module>
.root {
position: relative;
box-sizing: border-box;
padding: 24px 32px;
@ -165,139 +166,135 @@ useTooltip(reactionRef, (showing) => {
overflow-wrap: break-word;
display: flex;
contain: content;
}
> .head {
position: sticky;
top: 0;
flex-shrink: 0;
width: 42px;
height: 42px;
margin-right: 8px;
.head {
position: sticky;
top: 0;
flex-shrink: 0;
width: 42px;
height: 42px;
margin-right: 8px;
}
> .icon {
display: block;
width: 100%;
height: 100%;
border-radius: 6px;
}
.icon {
display: block;
width: 100%;
height: 100%;
border-radius: 6px;
}
> .sub-icon {
position: absolute;
z-index: 1;
bottom: -2px;
right: -2px;
width: 20px;
height: 20px;
box-sizing: border-box;
border-radius: 100%;
background: var(--panel);
box-shadow: 0 0 0 3px var(--panel);
font-size: 12px;
text-align: center;
.subIcon {
position: absolute;
z-index: 1;
bottom: -2px;
right: -2px;
width: 20px;
height: 20px;
box-sizing: border-box;
border-radius: 100%;
background: var(--panel);
box-shadow: 0 0 0 3px var(--panel);
font-size: 12px;
text-align: center;
color: #fff;
&:empty {
display: none;
}
> * {
color: #fff;
width: 100%;
height: 100%;
}
&.follow, &.followRequestAccepted, &.receiveFollowRequest, &.groupInvited {
padding: 3px;
background: #36aed2;
pointer-events: none;
}
&.renote {
padding: 3px;
background: #36d298;
pointer-events: none;
}
&.quote {
padding: 3px;
background: #36d298;
pointer-events: none;
}
&.reply {
padding: 3px;
background: #007aff;
pointer-events: none;
}
&.mention {
padding: 3px;
background: #88a6b7;
pointer-events: none;
}
&.pollEnded {
padding: 3px;
background: #88a6b7;
pointer-events: none;
}
}
}
> .tail {
flex: 1;
min-width: 0;
> header {
display: flex;
align-items: baseline;
white-space: nowrap;
> .name {
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
overflow: hidden;
}
> .time {
margin-left: auto;
font-size: 0.9em;
}
}
> .content {
> .text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
> i {
vertical-align: super;
font-size: 50%;
opacity: 0.5;
}
> i:first-child {
margin-right: 4px;
}
> i:last-child {
margin-left: 4px;
}
}
}
&:empty {
display: none;
}
}
.t_follow, .t_followRequestAccepted, .t_receiveFollowRequest, .t_groupInvited {
padding: 3px;
background: #36aed2;
pointer-events: none;
}
.t_renote {
padding: 3px;
background: #36d298;
pointer-events: none;
}
.t_quote {
padding: 3px;
background: #36d298;
pointer-events: none;
}
.t_reply {
padding: 3px;
background: #007aff;
pointer-events: none;
}
.t_mention {
padding: 3px;
background: #88a6b7;
pointer-events: none;
}
.t_pollEnded {
padding: 3px;
background: #88a6b7;
pointer-events: none;
}
.tail {
flex: 1;
min-width: 0;
}
.header {
display: flex;
align-items: baseline;
white-space: nowrap;
}
.headerName {
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
overflow: hidden;
}
.headerTime {
margin-left: auto;
font-size: 0.9em;
}
.content {
}
.text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.quote {
vertical-align: super;
font-size: 50%;
opacity: 0.5;
}
.quote:first-child {
margin-right: 4px;
}
.quote:last-child {
margin-left: 4px;
}
@container (max-width: 600px) {
.qglefbjs {
.root {
padding: 16px;
font-size: 0.9em;
}
}
@container (max-width: 500px) {
.qglefbjs {
.root {
padding: 12px;
font-size: 0.85em;
}

View File

@ -8,10 +8,10 @@
</template>
<template #default="{ items: notifications }">
<XList v-slot="{ item: notification }" class="elsfgstc" :items="notifications" :no-gap="true">
<MkDateSeparatedList 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"/>
<XNotification v-else :key="notification.id" :notification="notification" :with-time="true" :full="true" class="_panel notification"/>
</XList>
</MkDateSeparatedList>
</template>
</MkPagination>
</template>
@ -21,7 +21,7 @@ import { defineComponent, markRaw, onUnmounted, onMounted, computed, shallowRef
import { notificationTypes } from 'misskey-js';
import MkPagination, { Paging } from '@/components/MkPagination.vue';
import XNotification from '@/components/MkNotification.vue';
import XList from '@/components/MkDateSeparatedList.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import XNote from '@/components/MkNote.vue';
import * as os from '@/os';
import { stream } from '@/stream';

View File

@ -1,9 +1,7 @@
<template>
<Transition :name="$store.state.animation ? 'y' : ''">
<TransitionGroup v-if="Object.keys(note.reactions).length > 0" :name="$store.state.animation ? 'x' : ''" tag="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"/>
</TransitionGroup>
</Transition>
<TransitionGroup :name="$store.state.animation ? 'x' : ''" tag="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"/>
</TransitionGroup>
</template>
<script lang="ts" setup>
@ -22,16 +20,6 @@ const isMe = computed(() => $i && $i.id === props.note.userId);
</script>
<style lang="scss" scoped>
.y-enter-active, .y-leave-active {
overflow: clip;
max-height: 36px;
transition: opacity 0.2s cubic-bezier(0,.5,.5,1), max-height 0.2s cubic-bezier(0,.5,.5,1) !important;
}
.y-enter-from, .y-leave-to {
max-height: 0px;
opacity: 0;
}
.x-move, .x-enter-active, .x-leave-active {
transition: opacity 0.2s cubic-bezier(0,.5,.5,1), transform 0.2s cubic-bezier(0,.5,.5,1) !important;
}

View File

@ -1,6 +1,12 @@
<template>
<Transition :name="$store.state.animation ? 'tooltip' : ''" appear @after-leave="emit('closed')">
<div v-show="showing" ref="el" class="buebdbiu _acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
<Transition
:enter-active-class="$store.state.animation ? $style.transition_tooltip_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_tooltip_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_tooltip_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_tooltip_leaveTo : ''"
appear @after-leave="emit('closed')"
>
<div v-show="showing" ref="el" :class="$style.root" class="_acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
<slot>
<Mfm v-if="asMfm" :text="text"/>
<span v-else>{{ text }}</span>
@ -74,20 +80,20 @@ onUnmounted(() => {
});
</script>
<style lang="scss" scoped>
.tooltip-enter-active,
.tooltip-leave-active {
<style lang="scss" module>
.transition_tooltip_enterActive,
.transition_tooltip_leaveActive {
opacity: 1;
transform: scale(1);
transition: transform 200ms cubic-bezier(0.23, 1, 0.32, 1), opacity 200ms cubic-bezier(0.23, 1, 0.32, 1);
}
.tooltip-enter-from,
.tooltip-leave-active {
.transition_tooltip_enterFrom,
.transition_tooltip_leaveTo {
opacity: 0;
transform: scale(0.75);
}
.buebdbiu {
.root {
position: absolute;
font-size: 0.8em;
padding: 8px 12px;

View File

@ -15,21 +15,21 @@
handle=".handle"
:animation="150"
:group="{ name: 'SortableMkWidgets' }"
@update:model-value="v => emit('updateWidgets', v)"
:class="$style['edit-editing']"
@update:model-value="v => emit('updateWidgets', v)"
>
<template #item="{element}">
<div :class="[$style.widget, $style['customize-container']]">
<button :class="$style['customize-container-config']" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button>
<button :class="$style['customize-container-remove']" class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button>
<div class="handle">
<component :is="`mkw-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style['customize-container-handle-widget']" :widget="element" @update-props="updateWidget(element.id, $event)"/>
<component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style['customize-container-handle-widget']" :widget="element" @update-props="updateWidget(element.id, $event)"/>
</div>
</div>
</template>
</Sortable>
</template>
<component :is="`mkw-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @update-props="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
<component :is="`widget-${widget.name}`" v-for="widget in widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @update-props="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
</div>
</template>
<script lang="ts">

View File

@ -1,7 +1,7 @@
<template>
<span class="mk-acct">
<span class="name">@{{ user.username }}</span>
<span v-if="user.host || detail || $store.state.showFullAcct" class="host">@{{ user.host || host }}</span>
<span>
<span>@{{ user.username }}</span>
<span v-if="user.host || detail || $store.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span>
</span>
</template>
@ -18,10 +18,3 @@ defineProps<{
const host = toUnicode(hostRaw);
</script>
<style lang="scss" scoped>
.mk-acct {
> .host {
opacity: 0.5;
}
}
</style>

View File

@ -1,11 +1,11 @@
<template>
<span v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: $store.state.squareAvatars }" :style="{ color }" :title="acct(user)" @click="onClick">
<img class="inner" :src="url" decoding="async"/>
<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
<span v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: $store.state.squareAvatars }]" class="_noSelect" :style="{ color }" :title="acct(user)" @click="onClick">
<img :class="$style.inner" :src="url" decoding="async"/>
<MkUserOnlineIndicator v-if="showIndicator" :class="$style.indicator" :user="user"/>
</span>
<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: $store.state.squareAvatars }" :style="{ color }" :to="userPage(user)" :title="acct(user)" :target="target">
<img class="inner" :src="url" decoding="async"/>
<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="_noSelect" :class="[$style.root, { [$style.cat]: user.isCat, [$style.square]: $store.state.squareAvatars }]" :style="{ color }" :to="userPage(user)" :title="acct(user)" :target="target">
<img :class="$style.inner" :src="url" decoding="async"/>
<MkUserOnlineIndicator v-if="showIndicator" :class="$style.indicator" :user="user"/>
</MkA>
</template>
@ -52,7 +52,7 @@ watch(() => props.user.avatarBlurhash, () => {
});
</script>
<style lang="scss" scoped>
<style lang="scss" module>
@keyframes earwiggleleft {
from { transform: rotate(37.6deg) skew(30deg); }
25% { transform: rotate(10deg) skew(30deg); }
@ -69,74 +69,74 @@ watch(() => props.user.avatarBlurhash, () => {
to { transform: rotate(-37.6deg) skew(-30deg); }
}
.eiwwqkts {
.root {
position: relative;
display: inline-block;
vertical-align: bottom;
flex-shrink: 0;
border-radius: 100%;
line-height: 16px;
}
.inner {
position: absolute;
bottom: 0;
left: 0;
right: 0;
top: 0;
border-radius: 100%;
z-index: 1;
overflow: clip;
object-fit: cover;
width: 100%;
height: 100%;
}
.indicator {
position: absolute;
z-index: 1;
bottom: 0;
left: 0;
width: 20%;
height: 20%;
}
.square {
border-radius: 20%;
> .inner {
position: absolute;
bottom: 0;
left: 0;
right: 0;
top: 0;
border-radius: 100%;
z-index: 1;
overflow: clip;
object-fit: cover;
width: 100%;
height: 100%;
}
> .indicator {
position: absolute;
z-index: 1;
bottom: 0;
left: 0;
width: 20%;
height: 20%;
}
&.square {
border-radius: 20%;
}
}
> .inner {
border-radius: 20%;
}
.cat {
&:before, &:after {
background: #df548f;
border: solid 4px currentColor;
box-sizing: border-box;
content: '';
display: inline-block;
height: 50%;
width: 50%;
}
&.cat {
&:before, &:after {
background: #df548f;
border: solid 4px currentColor;
box-sizing: border-box;
content: '';
display: inline-block;
height: 50%;
width: 50%;
}
&:before {
border-radius: 0 75% 75%;
transform: rotate(37.5deg) skew(30deg);
}
&:after {
border-radius: 75% 0 75% 75%;
transform: rotate(-37.5deg) skew(-30deg);
}
&:hover {
&:before {
border-radius: 0 75% 75%;
transform: rotate(37.5deg) skew(30deg);
animation: earwiggleleft 1s infinite;
}
&:after {
border-radius: 75% 0 75% 75%;
transform: rotate(-37.5deg) skew(-30deg);
}
&:hover {
&:before {
animation: earwiggleleft 1s infinite;
}
&:after {
animation: earwiggleright 1s infinite;
}
animation: earwiggleright 1s infinite;
}
}
}

View File

@ -1,6 +1,6 @@
<template>
<img v-if="isCustom" class="mk-emoji custom" :class="{ normal, noStyle }" :src="url" :alt="alt" :title="alt" decoding="async"/>
<img v-else-if="char && !useOsNativeEmojis" class="mk-emoji" :src="url" :alt="alt" decoding="async" @pointerenter="computeTitle"/>
<img v-if="isCustom" :class="[$style.root, $style.custom, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async"/>
<img v-else-if="char && !useOsNativeEmojis" :class="$style.root" :src="url" :alt="alt" decoding="async" @pointerenter="computeTitle"/>
<span v-else-if="char && useOsNativeEmojis" :alt="alt" @pointerenter="computeTitle">{{ char }}</span>
<span v-else>{{ emoji }}</span>
</template>
@ -47,32 +47,32 @@ function computeTitle(event: PointerEvent): void {
}
</script>
<style lang="scss" scoped>
.mk-emoji {
<style lang="scss" module>
.root {
height: 1.25em;
vertical-align: -0.25em;
}
.custom {
height: 2.5em;
vertical-align: middle;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.2);
}
}
.normal {
height: 1.25em;
vertical-align: -0.25em;
&.custom {
height: 2.5em;
vertical-align: middle;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.2);
}
&.normal {
height: 1.25em;
vertical-align: -0.25em;
&:hover {
transform: none;
}
}
}
&.noStyle {
height: auto !important;
&:hover {
transform: none;
}
}
.noStyle {
height: auto !important;
}
</style>

View File

@ -1,19 +1,19 @@
<template>
<component
:is="self ? 'MkA' : 'a'" ref="el" class="ieqqeuvs _link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel" :target="target"
:is="self ? 'MkA' : 'a'" ref="el" :class="$style.root" class="_link" :[attr]="self ? props.url.substring(local.length) : props.url" :rel="rel" :target="target"
@contextmenu.stop="() => {}"
>
<template v-if="!self">
<span class="schema">{{ schema }}//</span>
<span class="hostname">{{ hostname }}</span>
<span v-if="port != ''" class="port">:{{ port }}</span>
<span :class="$style.schema">{{ schema }}//</span>
<span :class="$style.hostname">{{ hostname }}</span>
<span v-if="port != ''" :class="$style.port">:{{ port }}</span>
</template>
<template v-if="pathname === '/' && self">
<span class="self">{{ hostname }}</span>
<span :class="$style.self">{{ hostname }}</span>
</template>
<span v-if="pathname != ''" class="pathname">{{ self ? pathname.substring(1) : pathname }}</span>
<span class="query">{{ query }}</span>
<span class="hash">{{ hash }}</span>
<span v-if="pathname != ''" :class="$style.pathname">{{ self ? pathname.substring(1) : pathname }}</span>
<span :class="$style.query">{{ query }}</span>
<span :class="$style.hash">{{ hash }}</span>
<i v-if="target === '_blank'" class="ti ti-external-link icon"></i>
</component>
</template>
@ -53,37 +53,37 @@ const attr = self ? 'to' : 'href';
const target = self ? null : '_blank';
</script>
<style lang="scss" scoped>
.ieqqeuvs {
<style lang="scss" module>
.root {
word-break: break-all;
}
> .icon {
padding-left: 2px;
font-size: .9em;
}
.icon {
padding-left: 2px;
font-size: .9em;
}
> .self {
font-weight: bold;
}
.self {
font-weight: bold;
}
> .schema {
opacity: 0.5;
}
.schema {
opacity: 0.5;
}
> .hostname {
font-weight: bold;
}
.hostname {
font-weight: bold;
}
> .pathname {
opacity: 0.8;
}
.pathname {
opacity: 0.8;
}
> .query {
opacity: 0.5;
}
.query {
opacity: 0.5;
}
> .hash {
font-style: italic;
}
.hash {
font-style: italic;
}
</style>

View File

@ -5,7 +5,6 @@ import MkLink from '@/components/MkLink.vue';
import MkMention from '@/components/MkMention.vue';
import MkEmoji from '@/components/global/MkEmoji.vue';
import { concat } from '@/scripts/array';
import MkFormula from '@/components/MkFormula.vue';
import MkCode from '@/components/MkCode.vue';
import MkGoogle from '@/components/MkGoogle.vue';
import MkSparkle from '@/components/MkSparkle.vue';
@ -273,7 +272,7 @@ export default defineComponent({
key: Math.random(),
emoji: `:${token.props.name}:`,
normal: this.plain,
host: this.author?.host,
host: this.author.host,
})];
}
@ -286,19 +285,11 @@ export default defineComponent({
}
case 'mathInline': {
return [h(MkFormula, {
key: Math.random(),
formula: token.props.formula,
block: false,
})];
return [h('code', genEl(token.props.formula))];
}
case 'mathBlock': {
return [h(MkFormula, {
key: Math.random(),
formula: token.props.formula,
block: true,
})];
return [h('code', genEl(token.props.formula))];
}
case 'search': {

View File

@ -4,89 +4,11 @@ import { Component, markRaw, Ref, ref, defineAsyncComponent } from 'vue';
import { EventEmitter } from 'eventemitter3';
import insertTextAtCursor from 'insert-text-at-cursor';
import * as Misskey from 'misskey-js';
import { apiUrl, url } from '@/config';
import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
import MkWaitingDialog from '@/components/MkWaitingDialog.vue';
import { MenuItem } from '@/types/menu';
import { $i } from '@/account';
export const pendingApiRequestsCount = ref(0);
const apiClient = new Misskey.api.APIClient({
origin: url,
});
export const api = ((endpoint: string, data: Record<string, any> = {}, token?: string | null | undefined) => {
pendingApiRequestsCount.value++;
const onFinally = () => {
pendingApiRequestsCount.value--;
};
const promise = new Promise((resolve, reject) => {
// Append a credential
if ($i) (data as any).i = $i.token;
if (token !== undefined) (data as any).i = token;
// Send request
window.fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
method: 'POST',
body: JSON.stringify(data),
credentials: 'omit',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
},
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve();
} else {
reject(body.error);
}
}).catch(reject);
});
promise.then(onFinally, onFinally);
return promise;
}) as typeof apiClient.request;
export const apiGet = ((endpoint: string, data: Record<string, any> = {}) => {
pendingApiRequestsCount.value++;
const onFinally = () => {
pendingApiRequestsCount.value--;
};
const query = new URLSearchParams(data);
const promise = new Promise((resolve, reject) => {
// Send request
window.fetch(`${apiUrl}/${endpoint}?${query}`, {
method: 'GET',
credentials: 'omit',
cache: 'default',
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve();
} else {
reject(body.error);
}
}).catch(reject);
});
promise.then(onFinally, onFinally);
return promise;
}) as typeof apiClient.request;
import { pendingApiRequestsCount, api, apiGet } from '@/scripts/api';
export { pendingApiRequestsCount, api, apiGet };
export const apiWithDialog = ((
endpoint: string,

View File

@ -11,9 +11,9 @@
</template>
<template #default="{ items }">
<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false">
<MkDateSeparatedList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false">
<XNote :key="item.id" :note="item.note" :class="$style.note"/>
</XList>
</MkDateSeparatedList>
</template>
</MkPagination>
</MkSpacer>
@ -24,7 +24,7 @@
import { ref } from 'vue';
import MkPagination from '@/components/MkPagination.vue';
import XNote from '@/components/MkNote.vue';
import XList from '@/components/MkDateSeparatedList.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';

View File

@ -16,7 +16,7 @@
</template>
<template #default="{ items: messages, fetching: pFetching }">
<XList
<MkDateSeparatedList
v-if="messages.length > 0"
v-slot="{ item: message }"
:class="{ messages: true, 'deny-move-transition': pFetching }"
@ -25,7 +25,7 @@
reversed
>
<XMessage :key="message.id" :message="message" :is-group="group != null"/>
</XList>
</MkDateSeparatedList>
</template>
</MkPagination>
</div>
@ -55,7 +55,7 @@ import * as Misskey from 'misskey-js';
import * as Acct from 'misskey-js/built/acct';
import XMessage from './messaging-room.message.vue';
import XForm from './messaging-room.form.vue';
import XList from '@/components/MkDateSeparatedList.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue';
import { isBottomVisible, onScrollBottom, scrollToBottom } from '@/scripts/scroll';
import * as os from '@/os';
@ -351,7 +351,6 @@ definePageMetadata(computed(() => !fetching ? user ? {
z-index: 2;
bottom: 0;
padding-top: 8px;
bottom: calc(env(safe-area-inset-bottom, 0px) + 8px);
> .new-message {
width: 100%;

View File

@ -114,16 +114,6 @@
</div>
</div>
</div>
<div class="section">
<div class="title">{{ i18n.ts._mfm.inlineMath }}</div>
<div class="content">
<p>{{ i18n.ts._mfm.inlineMathDescription }}</p>
<div class="preview">
<Mfm :text="preview_inlineMath"/>
<MkTextarea v-model="preview_inlineMath"><template #label>MFM</template></MkTextarea>
</div>
</div>
</div>
<!-- deprecated
<div class="section">
<div class="title">{{ i18n.ts._mfm.search }}</div>
@ -329,7 +319,6 @@ let preview_small = $ref(`<small>${i18n.ts._mfm.dummy}</small>`);
let preview_center = $ref(`<center>${i18n.ts._mfm.dummy}</center>`);
let preview_inlineCode = $ref('`<: "Hello, world!"`');
let preview_blockCode = $ref('```\n~ (#i, 100) {\n\t<: ? ((i % 15) = 0) "FizzBuzz"\n\t\t.? ((i % 3) = 0) "Fizz"\n\t\t.? ((i % 5) = 0) "Buzz"\n\t\t. i\n}\n```');
let preview_inlineMath = $ref('\\(x= \\frac{-b\' \\pm \\sqrt{(b\')^2-ac}}{a}\\)');
let preview_quote = $ref(`> ${i18n.ts._mfm.dummy}`);
let preview_search = $ref(`${i18n.ts._mfm.dummy} 検索`);
let preview_jelly = $ref('$[jelly 🍮] $[jelly.speed=5s 🍮]');

View File

@ -2,15 +2,14 @@
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="headerTabs" :display-my-avatar="true"/></template>
<MkSpacer :content-max="800">
<div ref="rootEl" v-hotkey.global="keymap" class="cmuxhskf">
<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _panel" style="margin-bottom: var(--margin);"/>
<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
<div ref="rootEl" v-hotkey.global="keymap">
<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/>
<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
<div v-if="queue > 0" class="new"><button class="_buttonPrimary" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
<div class="tl">
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
<div :class="$style.tl">
<XTimeline
ref="tl" :key="src"
class="tl"
:src="src"
:sound="true"
@queue="queueUpdated"
@ -154,30 +153,28 @@ definePageMetadata(computed(() => ({
})));
</script>
<style lang="scss" scoped>
.cmuxhskf {
> .new {
position: sticky;
top: calc(var(--stickyTop, 0px) + 16px);
z-index: 1000;
width: 100%;
<style lang="scss" module>
.new {
position: sticky;
top: calc(var(--stickyTop, 0px) + 16px);
z-index: 1000;
width: 100%;
> button {
display: block;
margin: var(--margin) auto 0 auto;
padding: 8px 16px;
border-radius: 32px;
}
}
> .post-form {
border-radius: var(--radius);
}
> .tl {
background: var(--bg);
border-radius: var(--radius);
overflow: clip;
> button {
display: block;
margin: var(--margin) auto 0 auto;
padding: 8px 16px;
border-radius: 32px;
}
}
.postForm {
border-radius: var(--radius);
}
.tl {
background: var(--bg);
border-radius: var(--radius);
overflow: clip;
}
</style>

View File

@ -0,0 +1,79 @@
import { Endpoints } from 'misskey-js/built/api.types';
import { ref } from 'vue';
import { apiUrl } from '@/config';
import { $i } from '@/account';
export const pendingApiRequestsCount = ref(0);
// Implements Misskey.api.ApiClient.request
export function api<E extends keyof Endpoints, P extends Endpoints[E]['req']>(endpoint: E, data: P = {} as any, token?: string | null | undefined): Promise<Endpoints[E]['res']> {
pendingApiRequestsCount.value++;
const onFinally = () => {
pendingApiRequestsCount.value--;
};
const promise = new Promise<Endpoints[E]['res'] | void>((resolve, reject) => {
// Append a credential
if ($i) (data as any).i = $i.token;
if (token !== undefined) (data as any).i = token;
// Send request
window.fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
method: 'POST',
body: JSON.stringify(data),
credentials: 'omit',
cache: 'no-cache',
headers: {
'Content-Type': 'application/json',
},
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve();
} else {
reject(body.error);
}
}).catch(reject);
});
promise.then(onFinally, onFinally);
return promise;
}
// Implements Misskey.api.ApiClient.request
export function apiGet<E extends keyof Endpoints, P extends Endpoints[E]['req']>(endpoint: E, data: P = {} as any): Promise<Endpoints[E]['res']> {
pendingApiRequestsCount.value++;
const onFinally = () => {
pendingApiRequestsCount.value--;
};
const query = new URLSearchParams(data as any);
const promise = new Promise<Endpoints[E]['res'] | void>((resolve, reject) => {
// Send request
window.fetch(`${apiUrl}/${endpoint}?${query}`, {
method: 'GET',
credentials: 'omit',
cache: 'default',
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
resolve(body);
} else if (res.status === 204) {
resolve();
} else {
reject(body.error);
}
}).catch(reject);
});
promise.then(onFinally, onFinally);
return promise;
}

View File

@ -7,7 +7,7 @@ export class StickySidebar {
private isTop = false;
private isBottom = false;
private offsetTop: number;
private globalHeaderHeight: number = 59;
private globalHeaderHeight = 59;
constructor(container: StickySidebar['container'], marginTop = 0, globalHeaderHeight = 0) {
this.container = container;

View File

@ -6,9 +6,11 @@
--marginHalf: 10px;
--margin: var(--marginFull);
--minBottomSpacing: 0px;
@media (max-width: 500px) {
--margin: var(--marginHalf);
--minBottomSpacing: calc(72px + max(12px, env(safe-area-inset-bottom, 0px)));
}
//--ad: rgb(255 169 0 / 10%);

View File

@ -9,8 +9,15 @@
<XUpload v-if="uploads.length > 0"/>
<TransitionGroup :name="$store.state.animation ? 'notification' : ''" tag="div" class="notifications">
<XNotification v-for="notification in notifications" :key="notification.id" :notification="notification" class="notification"/>
<TransitionGroup
tag="div" :class="$style.notifications"
:move-class="$store.state.animation ? $style.transition_notification_move : ''"
:enter-active-class="$store.state.animation ? $style.transition_notification_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_notification_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_notification_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_notification_leaveTo : ''"
>
<XNotification v-for="notification in notifications" :key="notification.id" :notification="notification" :class="$style.notification"/>
</TransitionGroup>
<XStreamIndicator/>
@ -73,11 +80,14 @@ if ($i) {
}
</script>
<style lang="scss" scoped>
.notification-move, .notification-enter-active, .notification-leave-active {
<style lang="scss" module>
.transition_notification_move,
.transition_notification_enterActive,
.transition_notification_leaveActive {
transition: opacity 0.3s, transform 0.3s !important;
}
.notification-enter-from, .notification-leave-to {
.transition_notification_enterFrom,
.transition_notification_leaveTo {
opacity: 0;
transform: translateX(-250px);
}
@ -91,31 +101,28 @@ if ($i) {
padding: 0 32px;
pointer-events: none;
container-type: inline-size;
}
> .notification {
& + .notification {
margin-top: 8px;
}
.notification {
& + .notification {
margin-top: 8px;
}
}
@media (max-width: 700px) {
@media (max-width: 500px) {
.notifications {
top: initial;
bottom: 112px;
padding: 0 16px;
bottom: calc(var(--minBottomSpacing) + var(--margin));
padding: 0 var(--margin);
display: flex;
flex-direction: column-reverse;
> .notification {
& + .notification {
margin-top: 0;
margin-bottom: 8px;
}
}
}
@media (max-width: 500px) {
bottom: calc(env(safe-area-inset-bottom, 0px) + 92px);
padding: 0 8px;
.notification {
& + .notification {
margin-top: 0;
margin-bottom: 8px;
}
}
}
</style>

View File

@ -38,8 +38,8 @@ onUnmounted(() => {
.nsbbhtug {
position: fixed;
z-index: 16385;
bottom: 8px;
right: 8px;
bottom: calc(var(--minBottomSpacing) + var(--margin));
right: var(--margin);
margin: 0;
padding: 6px 12px;
font-size: 0.9em;

View File

@ -7,7 +7,7 @@
<XSidebar/>
</div>
<div v-else ref="widgetsLeft" class="widgets left">
<XWidgets place="left" :classic="true" @mounted="attachSticky(widgetsLeft)"/>
<XWidgets place="left" :margin-top="'var(--margin)'" @mounted="attachSticky(widgetsLeft)"/>
</div>
<main class="main" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu">
@ -17,7 +17,7 @@
</main>
<div v-if="isDesktop" ref="widgetsRight" class="widgets right">
<XWidgets :place="showMenuOnTop ? 'right' : null" :classic="true" @mounted="attachSticky(widgetsRight)"/>
<XWidgets :place="showMenuOnTop ? 'right' : null" :margin-top="showMenuOnTop ? '0' : 'var(--margin)'" @mounted="attachSticky(widgetsRight)"/>
</div>
</div>
@ -80,7 +80,7 @@ provide('shouldHeaderThin', showMenuOnTop);
provide('forceSpacerMin', true);
function attachSticky(el) {
const sticky = new StickySidebar(el, defaultStore.state.menuDisplay === 'top' ? 0 : 16, defaultStore.state.menuDisplay === 'top' ? 60 : 0); // TODO: 60px
const sticky = new StickySidebar(el, 0, defaultStore.state.menuDisplay === 'top' ? 60 : 0); // TODO: 60px
window.addEventListener('scroll', () => {
sticky.calc(window.scrollY);
}, { passive: true });
@ -248,7 +248,6 @@ onMounted(() => {
> .widgets {
//--panelBorder: none;
width: 300px;
margin-top: 16px;
@media (max-width: $widgets-hide-threshold) {
display: none;

View File

@ -1,17 +1,15 @@
<template>
<div
class="mk-deck" :class="[{ isMobile }]"
>
<div :class="[$style.root, { [$style.rootIsMobile]: isMobile }]">
<XSidebar v-if="!isMobile"/>
<div class="main">
<XStatusBars class="statusbars"/>
<div ref="columnsEl" class="columns" :class="deckStore.reactiveState.columnAlign.value" @contextmenu.self.prevent="onContextmenu">
<div :class="$style.main">
<XStatusBars/>
<div ref="columnsEl" :class="[$style.columns, deckStore.reactiveState.columnAlign.value]" @contextmenu.self.prevent="onContextmenu">
<template v-for="ids in layout">
<!-- sectionを利用しているのはdeck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
<section
v-if="ids.length > 1"
class="folder column"
:class="$style.folder"
: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)"/>
@ -20,51 +18,64 @@
v-else
:ref="ids[0]"
:key="ids[0]"
class="column"
:class="$style.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="layout.length === 0" class="intro _panel">
<div v-if="layout.length === 0" class="_panel" :class="$style.onboarding">
<div>{{ i18n.ts._deck.introduction }}</div>
<MkButton primary class="add" @click="addColumn">{{ i18n.ts._deck.addColumn }}</MkButton>
<MkButton primary style="margin: 1em auto;" @click="addColumn">{{ i18n.ts._deck.addColumn }}</MkButton>
<div>{{ i18n.ts._deck.introduction2 }}</div>
</div>
<div class="sideMenu">
<div class="top">
<button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${deckStore.state.profile}`" class="_button button" @click="changeProfile"><i class="ti ti-caret-down"></i></button>
<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" class="_button button" @click="deleteProfile"><i class="ti ti-trash"></i></button>
<div :class="$style.sideMenu">
<div :class="$style.sideMenuTop">
<button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${deckStore.state.profile}`" :class="$style.sideMenuButton" class="_button" @click="changeProfile"><i class="ti ti-caret-down"></i></button>
<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.sideMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button>
</div>
<div class="middle">
<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" class="_button button" @click="addColumn"><i class="ti ti-plus"></i></button>
<div :class="$style.sideMenuMiddle">
<button v-tooltip.noDelay.left="i18n.ts._deck.addColumn" :class="$style.sideMenuButton" class="_button" @click="addColumn"><i class="ti ti-plus"></i></button>
</div>
<div class="bottom">
<button v-tooltip.noDelay.left="i18n.ts.settings" class="_button button settings" @click="showSettings"><i class="ti ti-settings"></i></button>
<div :class="$style.sideMenuBottom">
<button v-tooltip.noDelay.left="i18n.ts.settings" :class="$style.sideMenuButton" class="_button" @click="showSettings"><i class="ti ti-settings"></i></button>
</div>
</div>
</div>
</div>
<div v-if="isMobile" class="buttons">
<button class="button nav _button" @click="drawerMenuShowing = true"><i class="ti ti-menu-2"></i><span v-if="menuIndicated" class="indicator"><i class="_indicatorCircle"></i></span></button>
<button class="button home _button" @click="mainRouter.push('/')"><i class="ti ti-home"></i></button>
<button class="button notifications _button" @click="mainRouter.push('/my/notifications')"><i class="ti ti-bell"></i><span v-if="$i?.hasUnreadNotification" class="indicator"><i class="_indicatorCircle"></i></span></button>
<button class="button post _button" @click="os.post()"><i class="ti ti-pencil"></i></button>
<div v-if="isMobile" :class="$style.nav">
<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
<button :class="$style.navButton" class="_button" @click="mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button>
<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"><i :class="$style.navButtonIcon" class="ti ti-bell"></i><span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
<button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button>
</div>
<Transition :name="$store.state.animation ? 'menu-back' : ''">
<Transition
:enter-active-class="$store.state.animation ? $style.transition_menuDrawerBg_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_menuDrawerBg_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_menuDrawerBg_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_menuDrawerBg_leaveTo : ''"
>
<div
v-if="drawerMenuShowing"
class="menu-back _modalBg"
:class="$style.menuBg"
class="_modalBg"
@click="drawerMenuShowing = false"
@touchstart.passive="drawerMenuShowing = false"
></div>
</Transition>
<Transition :name="$store.state.animation ? 'menu' : ''">
<XDrawerMenu v-if="drawerMenuShowing" class="menu"/>
<Transition
:enter-active-class="$store.state.animation ? $style.transition_menuDrawer_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_menuDrawer_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_menuDrawer_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_menuDrawer_leaveTo : ''"
>
<div v-if="drawerMenuShowing" :class="$style.menu">
<XDrawerMenu/>
</div>
</Transition>
<XCommon/>
@ -223,30 +234,30 @@ async function deleteProfile() {
}
</script>
<style lang="scss" scoped>
.menu-enter-active,
.menu-leave-active {
<style lang="scss" module>
.transition_menuDrawerBg_enterActive,
.transition_menuDrawerBg_leaveActive {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_menuDrawerBg_enterFrom,
.transition_menuDrawerBg_leaveTo {
opacity: 0;
}
.transition_menuDrawer_enterActive,
.transition_menuDrawer_leaveActive {
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 {
.transition_menuDrawer_enterFrom,
.transition_menuDrawer_leaveTo {
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 {
.root {
$nav-hide-threshold: 650px; // TODO:
--margin: var(--marginHalf);
@ -257,178 +268,170 @@ async function deleteProfile() {
height: 100dvh;
box-sizing: border-box;
flex: 1;
}
&.isMobile {
padding-bottom: 100px;
}
.rootIsMobile {
padding-bottom: 100px;
}
> .main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
.main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
}
> .columns {
flex: 1;
display: flex;
overflow-x: auto;
overflow-y: clip;
.columns {
flex: 1;
display: flex;
overflow-x: auto;
overflow-y: clip;
&.center {
> .column:first-of-type {
margin-left: auto;
}
> .column:last-of-type {
margin-right: auto;
}
}
> .column {
flex-shrink: 0;
border-right: solid var(--deckDividerThickness) var(--deckDivider);
&:first-of-type {
border-left: solid var(--deckDividerThickness) var(--deckDivider);
}
&.folder {
display: flex;
flex-direction: column;
> *:not(:last-of-type) {
border-bottom: solid var(--deckDividerThickness) var(--deckDivider);
}
}
}
> .intro {
padding: 32px;
height: min-content;
text-align: center;
margin: auto;
> .add {
margin: 1em auto;
}
}
> .sideMenu {
flex-shrink: 0;
margin-right: 0;
margin-left: auto;
display: flex;
flex-direction: column;
justify-content: center;
width: 32px;
> .top, > .middle, > .bottom {
> .button {
display: block;
width: 100%;
aspect-ratio: 1;
}
}
> .top {
margin-bottom: auto;
}
> .middle {
margin-top: auto;
margin-bottom: auto;
}
> .bottom {
margin-top: auto;
}
}
&.center {
> .column:first-of-type {
margin-left: auto;
}
}
> .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;
}
}
> .column:last-of-type {
margin-right: auto;
}
}
> .menu-back {
z-index: 1001;
}
> .menu {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
height: 100dvh;
width: 240px;
box-sizing: border-box;
contain: strict;
overflow: auto;
overscroll-behavior: contain;
background: var(--navBg);
}
}
.column {
flex-shrink: 0;
border-right: solid var(--deckDividerThickness) var(--deckDivider);
&:first-of-type {
border-left: solid var(--deckDividerThickness) var(--deckDivider);
}
}
.folder {
composes: column;
display: flex;
flex-direction: column;
> *:not(:last-of-type) {
border-bottom: solid var(--deckDividerThickness) var(--deckDivider);
}
}
.onboarding {
padding: 32px;
height: min-content;
text-align: center;
margin: auto;
}
.sideMenu {
flex-shrink: 0;
margin-right: 0;
margin-left: auto;
display: flex;
flex-direction: column;
justify-content: center;
width: 32px;
}
.sideMenuButton {
display: block;
width: 100%;
aspect-ratio: 1;
}
.sideMenuTop {
margin-bottom: auto;
}
.sideMenuMiddle {
margin-top: auto;
margin-bottom: auto;
}
.sideMenuBottom {
margin-top: auto;
}
.menuBg {
z-index: 1001;
}
.menu {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
height: 100dvh;
width: 240px;
box-sizing: border-box;
contain: strict;
overflow: auto;
overscroll-behavior: contain;
background: var(--navBg);
}
.nav {
position: fixed;
z-index: 1000;
bottom: 0;
left: 0;
padding: 12px 12px max(12px, env(safe-area-inset-bottom, 0px)) 12px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr;
grid-gap: 8px;
width: 100%;
box-sizing: border-box;
-webkit-backdrop-filter: var(--blur, blur(32px));
backdrop-filter: var(--blur, blur(32px));
background-color: var(--header);
border-top: solid 0.5px var(--divider);
}
.navButton {
position: relative;
padding: 0;
aspect-ratio: 1;
width: 100%;
max-width: 60px;
margin: auto;
border-radius: 100%;
background: var(--panel);
color: var(--fg);
&:hover {
background: var(--panelHighlight);
}
&:active {
background: var(--X2);
}
}
.postButton {
composes: navButton;
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
color: var(--fgOnAccent);
&:hover {
background: linear-gradient(90deg, var(--X8), var(--X8));
}
&:active {
background: linear-gradient(90deg, var(--X8), var(--X8));
}
}
.navButtonIcon {
font-size: 18px;
}
.navButtonIndicator {
position: absolute;
top: 0;
left: 0;
color: var(--indicator);
font-size: 16px;
animation: blink 1s infinite;
}
</style>

View File

@ -1,31 +1,28 @@
<template>
<!-- sectionを利用しているのはdeck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
<section
v-hotkey="keymap" class="dnpfarvg"
:class="{ paged: isMainColumn, naked, active, isStacked, draghover, dragging, dropready }"
v-hotkey="keymap"
:class="[$style.root, { [$style.paged]: isMainColumn, [$style.naked]: naked, [$style.active]: active, [$style.isStacked]: isStacked, [$style.draghover]: draghover, [$style.dragging]: dragging, [$style.dropready]: dropready }]"
@dragover.prevent.stop="onDragover"
@dragleave="onDragleave"
@drop.prevent.stop="onDrop"
>
<header
:class="{ indicated }"
:class="[$style.header]"
draggable="true"
@click="goTop"
@dragstart="onDragstart"
@dragend="onDragend"
@contextmenu.prevent.stop="onContextmenu"
>
<button v-if="isStacked && !isMainColumn" class="toggleActive _button" @click="toggleActive">
<button v-if="isStacked && !isMainColumn" :class="$style.toggleActive" class="_button" @click="toggleActive">
<template v-if="active"><i class="ti ti-chevron-up"></i></template>
<template v-else><i class="ti ti-chevron-down"></i></template>
</button>
<div class="action">
<slot name="action"></slot>
</div>
<span class="header"><slot name="header"></slot></span>
<button v-tooltip="i18n.ts.settings" class="menu _button" @click.stop="showSettingsMenu"><i class="ti ti-dots"></i></button>
<span :class="$style.title"><slot name="header"></slot></span>
<button v-tooltip="i18n.ts.settings" :class="$style.menu" class="_button" @click.stop="showSettingsMenu"><i class="ti ti-dots"></i></button>
</header>
<div v-show="active" ref="body">
<div v-show="active" ref="body" :class="$style.body">
<slot></slot>
</div>
</section>
@ -46,12 +43,10 @@ const props = withDefaults(defineProps<{
column: Column;
isStacked?: boolean;
naked?: boolean;
indicated?: boolean;
menu?: MenuItem[];
}>(), {
isStacked: false,
naked: false,
indicated: false,
});
const emit = defineEmits<{
@ -245,8 +240,8 @@ function onDrop(ev) {
}
</script>
<style lang="scss" scoped>
.dnpfarvg {
<style lang="scss" module>
.root {
--root-margin: 10px;
--deckColumnHeaderHeight: 40px;
@ -292,10 +287,6 @@ function onDrop(ev) {
&:not(.active) {
flex-basis: var(--deckColumnHeaderHeight);
min-height: var(--deckColumnHeaderHeight);
> header.indicated {
box-shadow: 4px 0px var(--accent) inset;
}
}
&.naked {
@ -303,97 +294,78 @@ function onDrop(ev) {
-webkit-backdrop-filter: var(--blur, blur(10px));
backdrop-filter: var(--blur, blur(10px));
> header {
> .header {
background: transparent;
box-shadow: none;
> button {
color: var(--fg);
}
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;
.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);
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;
container-type: inline-size;
background-color: var(--bg);
&, * {
user-select: none;
}
}
.title {
display: inline-block;
align-items: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
.toggleActive,
.menu {
z-index: 1;
width: var(--deckColumnHeaderHeight);
line-height: var(--deckColumnHeaderHeight);
color: var(--faceTextButton);
&:hover {
color: var(--faceTextButtonHover);
}
&:active {
color: var(--faceTextButtonActive);
}
}
.toggleActive {
margin-left: -16px;
}
.menu {
margin-left: auto;
margin-right: -16px;
}
.body {
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;
container-type: inline-size;
background-color: var(--bg);
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<XColumn :menu="menu" :column="column" :is-stacked="isStacked" :indicated="indicated" @change-active-state="onChangeActiveState" @parent-focus="$event => emit('parent-focus', $event)">
<XColumn :menu="menu" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
<template #header>
<i v-if="column.tl === 'home'" class="ti ti-home"></i>
<i v-else-if="column.tl === 'local'" class="ti ti-planet"></i>
@ -15,7 +15,7 @@
</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"/>
<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')"/>
</XColumn>
</template>
@ -40,8 +40,6 @@ const emit = defineEmits<{
}>();
let disabled = $ref(false);
let indicated = $ref(false);
let columnActive = $ref(true);
onMounted(() => {
if (props.column.tl == null) {
@ -77,26 +75,6 @@ async function setType() {
});
}
function queueUpdated(q) {
if (columnActive) {
indicated = q !== 0;
}
}
function onNote() {
if (!columnActive) {
indicated = true;
}
}
function onChangeActiveState(state) {
columnActive = state;
if (columnActive) {
indicated = false;
}
}
const menu = [{
icon: 'ti ti-pencil',
text: i18n.ts.timeline,

View File

@ -1,8 +1,8 @@
<template>
<div class="dkgtipfy" :class="{ wallpaper }">
<XSidebar v-if="!isMobile" class="sidebar"/>
<div :class="[$style.root, { [$style.withWallpaper]: wallpaper }]">
<XSidebar v-if="!isMobile" :class="$style.sidebar"/>
<MkStickyContainer class="contents">
<MkStickyContainer :class="$style.contents">
<template #header><XStatusBars :class="$style.statusbars"/></template>
<main style="min-width: 0;" :style="{ background: pageMetadata?.value?.bg }" @contextmenu.stop="onContextmenu">
<div :class="$style.content" style="container-type: inline-size;">
@ -12,44 +12,71 @@
</main>
</MkStickyContainer>
<div v-if="isDesktop" ref="widgetsEl" class="widgets">
<XWidgets @mounted="attachSticky"/>
<div v-if="isDesktop" ref="widgetsEl" :class="$style.widgets">
<XWidgets :margin-top="'var(--margin)'" @mounted="attachSticky"/>
</div>
<button v-if="!isDesktop && !isMobile" class="widgetButton _button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button>
<button v-if="!isDesktop && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button>
<div v-if="isMobile" class="buttons">
<button class="button nav _button" @click="drawerMenuShowing = true"><i class="icon ti ti-menu-2"></i><span v-if="menuIndicated" class="indicator"><i class="_indicatorCircle"></i></span></button>
<button class="button home _button" @click="mainRouter.currentRoute.value.name === 'index' ? top() : mainRouter.push('/')"><i class="icon ti ti-home"></i></button>
<button class="button notifications _button" @click="mainRouter.push('/my/notifications')"><i class="icon ti ti-bell"></i><span v-if="$i?.hasUnreadNotification" class="indicator"><i class="_indicatorCircle"></i></span></button>
<button class="button widget _button" @click="widgetsShowing = true"><i class="icon ti ti-apps"></i></button>
<button class="button post _button" @click="os.post()"><i class="icon ti ti-pencil"></i></button>
<div v-if="isMobile" :class="$style.nav">
<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
<button :class="$style.navButton" class="_button" @click="mainRouter.currentRoute.value.name === 'index' ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button>
<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"><i :class="$style.navButtonIcon" class="ti ti-bell"></i><span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
<button :class="$style.navButton" class="_button" @click="widgetsShowing = true"><i :class="$style.navButtonIcon" class="ti ti-apps"></i></button>
<button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button>
</div>
<Transition :name="$store.state.animation ? 'menuDrawer-back' : ''">
<Transition
:enter-active-class="$store.state.animation ? $style.transition_menuDrawerBg_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_menuDrawerBg_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_menuDrawerBg_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_menuDrawerBg_leaveTo : ''"
>
<div
v-if="drawerMenuShowing"
class="menuDrawer-back _modalBg"
:class="$style.menuDrawerBg"
class="_modalBg"
@click="drawerMenuShowing = false"
@touchstart.passive="drawerMenuShowing = false"
></div>
</Transition>
<Transition :name="$store.state.animation ? 'menuDrawer' : ''">
<XDrawerMenu v-if="drawerMenuShowing" class="menuDrawer"/>
<Transition
:enter-active-class="$store.state.animation ? $style.transition_menuDrawer_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_menuDrawer_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_menuDrawer_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_menuDrawer_leaveTo : ''"
>
<div v-if="drawerMenuShowing" :class="$style.menuDrawer">
<XDrawerMenu/>
</div>
</Transition>
<Transition :name="$store.state.animation ? 'widgetsDrawer-back' : ''">
<Transition
:enter-active-class="$store.state.animation ? $style.transition_widgetsDrawerBg_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_widgetsDrawerBg_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_widgetsDrawerBg_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_widgetsDrawerBg_leaveTo : ''"
>
<div
v-if="widgetsShowing"
class="widgetsDrawer-back _modalBg"
:class="$style.widgetsDrawerBg"
class="_modalBg"
@click="widgetsShowing = false"
@touchstart.passive="widgetsShowing = false"
></div>
</Transition>
<Transition :name="$store.state.animation ? 'widgetsDrawer' : ''">
<XWidgets v-if="widgetsShowing" class="widgetsDrawer"/>
<Transition
:enter-active-class="$store.state.animation ? $style.transition_widgetsDrawer_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_widgetsDrawer_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_widgetsDrawer_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_widgetsDrawer_leaveTo : ''"
>
<div v-if="widgetsShowing" :class="$style.widgetsDrawer">
<button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i class="ti ti-x"></i></button>
<XWidgets/>
</div>
</Transition>
<XCommon/>
@ -174,195 +201,210 @@ function top() {
const wallpaper = miLocalStorage.getItem('wallpaper') != null;
</script>
<style lang="scss" scoped>
.widgetsDrawer-enter-active,
.widgetsDrawer-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);
}
.widgetsDrawer-enter-from,
.widgetsDrawer-leave-active {
opacity: 0;
transform: translateX(240px);
}
<style lang="scss" module>
$ui-font-size: 1em; // TODO:
$widgets-hide-threshold: 1090px;
.widgetsDrawer-back-enter-active,
.widgetsDrawer-back-leave-active {
.transition_menuDrawerBg_enterActive,
.transition_menuDrawerBg_leaveActive {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.widgetsDrawer-back-enter-from,
.widgetsDrawer-back-leave-active {
.transition_menuDrawerBg_enterFrom,
.transition_menuDrawerBg_leaveTo {
opacity: 0;
}
.menuDrawer-enter-active,
.menuDrawer-leave-active {
.transition_menuDrawer_enterActive,
.transition_menuDrawer_leaveActive {
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);
}
.menuDrawer-enter-from,
.menuDrawer-leave-active {
.transition_menuDrawer_enterFrom,
.transition_menuDrawer_leaveTo {
opacity: 0;
transform: translateX(-240px);
}
.menuDrawer-back-enter-active,
.menuDrawer-back-leave-active {
.transition_widgetsDrawerBg_enterActive,
.transition_widgetsDrawerBg_leaveActive {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.menuDrawer-back-enter-from,
.menuDrawer-back-leave-active {
.transition_widgetsDrawerBg_enterFrom,
.transition_widgetsDrawerBg_leaveTo {
opacity: 0;
}
.dkgtipfy {
$ui-font-size: 1em; // TODO:
$widgets-hide-threshold: 1090px;
.transition_widgetsDrawer_enterActive,
.transition_widgetsDrawer_leaveActive {
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);
}
.transition_widgetsDrawer_enterFrom,
.transition_widgetsDrawer_leaveTo {
opacity: 0;
transform: translateX(240px);
}
.root {
min-height: 100dvh;
box-sizing: border-box;
display: flex;
}
&.wallpaper {
background: var(--wallpaperOverlay);
//backdrop-filter: var(--blur, blur(4px));
}
.withWallpaper {
background: var(--wallpaperOverlay);
//backdrop-filter: var(--blur, blur(4px));
}
> .sidebar {
border-right: solid 0.5px var(--divider);
}
.sidebar {
border-right: solid 0.5px var(--divider);
}
> .contents {
width: 100%;
min-width: 0;
background: var(--bg);
}
.contents {
width: 100%;
min-width: 0;
background: var(--bg);
}
> .widgets {
padding: 0 var(--margin);
border-left: solid 0.5px var(--divider);
background: var(--bg);
.widgets {
padding: 0 var(--margin);
border-left: solid 0.5px var(--divider);
background: var(--bg);
@media (max-width: $widgets-hide-threshold) {
display: none;
}
}
> .widgetButton {
display: block;
position: fixed;
z-index: 1000;
bottom: 32px;
right: 32px;
width: 64px;
height: 64px;
border-radius: 100%;
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
font-size: 22px;
background: var(--panel);
}
> .widgetsDrawer-back {
z-index: 1001;
}
> .widgetsDrawer {
position: fixed;
top: 0;
right: 0;
z-index: 1001;
height: 100dvh;
padding: var(--margin) !important;
box-sizing: border-box;
overflow: auto;
overscroll-behavior: contain;
background: var(--bg);
}
> .buttons {
position: fixed;
z-index: 1000;
bottom: 0;
left: 0;
padding: 12px 12px max(12px, env(safe-area-inset-bottom, 0px)) 12px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
grid-gap: 8px;
width: 100%;
box-sizing: border-box;
-webkit-backdrop-filter: var(--blur, blur(32px));
backdrop-filter: var(--blur, blur(32px));
background-color: var(--header);
border-top: solid 0.5px var(--divider);
> .button {
position: relative;
padding: 0;
aspect-ratio: 1;
width: 100%;
max-width: 60px;
margin: auto;
border-radius: 100%;
background: var(--panel);
color: var(--fg);
&:hover {
background: var(--X2);
}
> .indicator {
position: absolute;
top: 0;
left: 0;
color: var(--indicator);
font-size: 16px;
animation: blink 1s infinite;
}
> .icon {
font-size: 18px;
}
&:disabled {
cursor: default;
> .icon {
opacity: 0.5;
}
}
&.post {
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
color: var(--fgOnAccent);
}
}
}
> .menuDrawer-back {
z-index: 1001;
}
> .menuDrawer {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
height: 100dvh;
width: 240px;
box-sizing: border-box;
contain: strict;
overflow: auto;
overscroll-behavior: contain;
background: var(--navBg);
@media (max-width: $widgets-hide-threshold) {
display: none;
}
}
</style>
<style lang="scss" module>
.widgetButton {
display: block;
position: fixed;
z-index: 1000;
bottom: 32px;
right: 32px;
width: 64px;
height: 64px;
border-radius: 100%;
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
font-size: 22px;
background: var(--panel);
}
.widgetsDrawerBg {
z-index: 1001;
}
.widgetsDrawer {
position: fixed;
top: 0;
right: 0;
z-index: 1001;
height: 100dvh;
padding: var(--margin) !important;
box-sizing: border-box;
overflow: auto;
overscroll-behavior: contain;
background: var(--bg);
}
.widgetsCloseButton {
padding: 8px;
display: block;
margin: 0 auto;
}
@media (min-width: 370px) {
.widgetsCloseButton {
display: none;
}
}
.nav {
position: fixed;
z-index: 1000;
bottom: 0;
left: 0;
padding: 12px 12px max(12px, env(safe-area-inset-bottom, 0px)) 12px;
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
grid-gap: 8px;
width: 100%;
box-sizing: border-box;
-webkit-backdrop-filter: var(--blur, blur(32px));
backdrop-filter: var(--blur, blur(32px));
background-color: var(--header);
border-top: solid 0.5px var(--divider);
}
.navButton {
position: relative;
padding: 0;
aspect-ratio: 1;
width: 100%;
max-width: 60px;
margin: auto;
border-radius: 100%;
background: var(--panel);
color: var(--fg);
&:hover {
background: var(--panelHighlight);
}
&:active {
background: var(--X2);
}
}
.postButton {
composes: navButton;
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
color: var(--fgOnAccent);
&:hover {
background: linear-gradient(90deg, var(--X8), var(--X8));
}
&:active {
background: linear-gradient(90deg, var(--X8), var(--X8));
}
}
.navButtonIcon {
font-size: 18px;
}
.navButtonIndicator {
position: absolute;
top: 0;
left: 0;
color: var(--indicator);
font-size: 16px;
animation: blink 1s infinite;
}
.menuDrawerBg {
z-index: 1001;
}
.menuDrawer {
position: fixed;
top: 0;
left: 0;
z-index: 1001;
height: 100dvh;
width: 240px;
box-sizing: border-box;
contain: strict;
overflow: auto;
overscroll-behavior: contain;
background: var(--navBg);
}
.statusbars {
position: sticky;
top: 0;
@ -370,12 +412,6 @@ const wallpaper = miLocalStorage.getItem('wallpaper') != null;
}
.spacer {
$widgets-hide-threshold: 1090px;
height: calc(env(safe-area-inset-bottom, 0px) + 96px);
@media (min-width: ($widgets-hide-threshold + 1px)) {
display: none;
}
height: calc(var(--minBottomSpacing));
}
</style>

View File

@ -1,6 +1,6 @@
<template>
<div class="efzpzdvf" :class="{ universal: !classic, classic }">
<XWidgets :edit="editMode" :widgets="widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
<div :class="$style.root" :style="{ paddingTop: marginTop }">
<XWidgets :class="$style.widgets" :edit="editMode" :widgets="widgets" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="editMode = false"/>
<button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button>
<button v-else class="_textButton mk-widget-edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button>
@ -21,10 +21,10 @@ const props = withDefaults(defineProps<{
// left = place: left
// right = rightnull
place?: 'left' | null | 'right';
classic?: boolean;
marginTop?: string;
}>(), {
place: null,
classic: false,
marginTop: '0',
});
const emit = defineEmits<{
@ -81,31 +81,14 @@ function updateWidgets(thisWidgets) {
}
</script>
<style lang="scss" scoped>
.efzpzdvf {
<style lang="scss" module>
.root {
position: sticky;
height: min-content;
min-height: 100vh;
box-sizing: border-box;
}
&.universal {
padding-top: var(--margin);
> * {
margin: var(--margin) 0;
}
}
> * {
width: 300px;
&:first-child {
margin-top: 0;
}
}
> .add {
margin: 0 auto;
}
.widgets {
width: 300px;
}
</style>

View File

@ -16,8 +16,8 @@
<script lang="ts" setup>
import { onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import XCalendar from './activity.calendar.vue';
import XChart from './activity.chart.vue';
import XCalendar from './WidgetActivity.calendar.vue';
import XChart from './WidgetActivity.chart.vue';
import { GetFormResultType } from '@/scripts/form';
import * as os from '@/os';
import MkContainer from '@/components/MkContainer.vue';

View File

@ -0,0 +1,94 @@
<template>
<div class="_panel">
<div :class="$style.container" :style="{ backgroundImage: $instance.bannerUrl ? `url(${ $instance.bannerUrl })` : null }">
<div :class="$style.iconContainer">
<img :src="$instance.iconUrl ?? $instance.faviconUrl ?? '/favicon.ico'" alt="" :class="$style.icon"/>
</div>
<div :class="$style.bodyContainer">
<div :class="$style.body">
<MkA :class="$style.name" to="/about" behavior="window">{{ $instance.name }}</MkA>
<div :class="$style.host">{{ host }}</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import { GetFormResultType } from '@/scripts/form';
import { host } from '@/config';
const name = 'instanceInfo';
const widgetPropsDef = {
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>
<style lang="scss" module>
.container {
position: relative;
background-size: cover;
background-position: center;
display: flex;
}
.iconContainer {
display: inline-block;
text-align: center;
padding: 16px;
}
.icon {
display: inline-block;
width: 60px;
height: 60px;
border-radius: 8px;
box-sizing: border-box;
border: solid 3px #fff;
}
.bodyContainer {
display: flex;
align-items: center;
min-width: 0;
padding: 0 16px 0 0;
}
.body {
text-overflow: ellipsis;
overflow: clip;
}
.name {
color: #fff;
filter: drop-shadow(0 0 4px #000);
font-weight: bold;
}
.host {
color: #fff;
filter: drop-shadow(0 0 4px #000);
}
</style>

View File

@ -0,0 +1,96 @@
<template>
<div class="_panel">
<div :class="$style.container" :style="{ backgroundImage: $i.bannerUrl ? `url(${ $i.bannerUrl })` : null }">
<div :class="$style.avatarContainer">
<MkAvatar :class="$style.avatar" :user="$i" :disable-link="true" :disable-preview="true"/>
</div>
<div :class="$style.bodyContainer">
<div :class="$style.body">
<MkA :class="$style.name" :to="userPage($i)">
<MkUserName :user="$i"/>
</MkA>
<div :class="$style.username"><MkAcct :user="$i" detail/></div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, Ref, ref, watch } from 'vue';
import { useWidgetPropsManager, Widget, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget';
import { GetFormResultType } from '@/scripts/form';
import { $i } from '@/account';
import { userPage } from '@/filters/user';
const name = 'profile';
const widgetPropsDef = {
};
type WidgetProps = GetFormResultType<typeof widgetPropsDef>;
// vueimporttype
//const props = defineProps<WidgetComponentProps<WidgetProps>>();
//const emit = defineEmits<WidgetComponentEmits<WidgetProps>>();
const props = defineProps<{ widget?: Widget<WidgetProps>; }>();
const emit = defineEmits<{ (ev: 'updateProps', props: WidgetProps); }>();
const { widgetProps, configure } = useWidgetPropsManager(name,
widgetPropsDef,
props,
emit,
);
defineExpose<WidgetComponentExpose>({
name,
configure,
id: props.widget ? props.widget.id : null,
});
</script>
<style lang="scss" module>
.container {
position: relative;
background-size: cover;
background-position: center;
display: flex;
}
.avatarContainer {
display: inline-block;
text-align: center;
padding: 16px;
}
.avatar {
display: inline-block;
width: 60px;
height: 60px;
box-sizing: border-box;
border: solid 3px #fff;
}
.bodyContainer {
display: flex;
align-items: center;
min-width: 0;
padding: 0 16px 0 0;
}
.body {
text-overflow: ellipsis;
overflow: clip;
}
.name {
color: #fff;
filter: drop-shadow(0 0 4px #000);
font-weight: bold;
}
.username {
color: #fff;
filter: drop-shadow(0 0 4px #000);
}
</style>

View File

@ -108,10 +108,7 @@ const tick = () => {
window.fetch(fetchEndpoint.value, {})
.then(res => res.json())
.then(feed => {
if (widgetProps.shuffle) {
shuffle(feed.items);
}
rawItems.value = feed.items;
rawItems.value = feed.items ?? [];
fetching.value = false;
key++;
});

Some files were not shown because too many files have changed in this diff Show More