Compare commits
24 Commits
13.0.0-bet
...
13.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f6a41cea7 | ||
|
|
0d7ee20a77 | ||
|
|
dcca2350dd | ||
|
|
1cfdd4c41a | ||
|
|
25f4ee7030 | ||
|
|
5320f23017 | ||
|
|
4ffbbbe6d8 | ||
|
|
132e45dff4 | ||
|
|
01652b72b3 | ||
|
|
8b1fdb5a3b | ||
|
|
192add376c | ||
|
|
244ea9593a | ||
|
|
f20d7cba74 | ||
|
|
a3e282bc75 | ||
|
|
49a95c34bf | ||
|
|
ecbefce2aa | ||
|
|
91356b1805 | ||
|
|
2e2ed1385f | ||
|
|
49f3090edd | ||
|
|
4594fb11de | ||
|
|
b93e56d2e5 | ||
|
|
c550dafb81 | ||
|
|
8709574f3d | ||
|
|
1b7043fa79 |
@@ -30,6 +30,7 @@ You should also include the user name that made the change.
|
||||
|
||||
#### For users
|
||||
- ノートのウォッチ機能が削除されました
|
||||
- アンケートに投票された際に通知が作成されなくなりました
|
||||
- 新たに動的なPagesを作ることはできなくなりました
|
||||
- 代わりにAiScriptを用いてより柔軟に動的なコンテンツを作成できるMisskey Play機能が実装されています。
|
||||
- AiScriptが0.12.2にアップデートされました
|
||||
@@ -77,9 +78,12 @@ You should also include the user name that made the change.
|
||||
- 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: add user list widget @syuilo
|
||||
- Client: add heatmap of daily active users to about page @syuilo
|
||||
- Client: introduce fluent emoji @syuilo
|
||||
- Client: add new theme @syuilo
|
||||
- Client: show fireworks when visit user who today is birthday @syuilo
|
||||
- Client: show bot warning on screen when logged in as bot account @syuilo
|
||||
- Client: improve overall performance of client @syuilo
|
||||
|
||||
@@ -920,6 +920,10 @@ like: "Gefällt mir"
|
||||
unlike: "\"Gefällt mir\" entfernen"
|
||||
numberOfLikes: "\"Gefällt mir\"-Anzahl"
|
||||
show: "Anzeigen"
|
||||
neverShow: "Nicht wieder anzeigen"
|
||||
remindMeLater: "Vielleicht später"
|
||||
didYouLikeMisskey: "Gefällt dir Misskey?"
|
||||
pleaseDonate: "Misskey ist die kostenlose Software, die von {host} verwendet wird. Wir würden uns über Spenden freuen, damit dessen Entwicklung weitergeführt werden kann!"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Ermöglicht eine Erleichterung der Servermoderation durch die automatische Erkennungen von NSFW-Medien unter Verwendung von Machine Learning. Hierdurch wird die Serverlast etwas erhöht."
|
||||
sensitivity: "Erkennungssensitivität"
|
||||
|
||||
@@ -920,6 +920,10 @@ like: "Like"
|
||||
unlike: "Unlike"
|
||||
numberOfLikes: "Likes"
|
||||
show: "Show"
|
||||
neverShow: "Don't show again"
|
||||
remindMeLater: "Maybe later"
|
||||
didYouLikeMisskey: "Have you taken a liking to Misskey?"
|
||||
pleaseDonate: "{host} uses the free software, Misskey. We would highly appreciate your donations so development of Misskey can continue!"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Reduces the effort of server moderation through automatically recognizing NSFW media via Machine Learning. This will slightly increase the load on the server."
|
||||
sensitivity: "Detection sensitivity"
|
||||
|
||||
@@ -28,7 +28,7 @@ timeline: "Timeline"
|
||||
noAccountDescription: "L'utente non ha ancora scritto niente nella biografia di profilo."
|
||||
login: "Accedi"
|
||||
loggingIn: "Accesso in corso..."
|
||||
logout: "Esci"
|
||||
logout: "Uscita"
|
||||
signup: "Iscriviti"
|
||||
uploading: "Caricamento..."
|
||||
save: "Salva"
|
||||
|
||||
@@ -1550,7 +1550,6 @@ _notification:
|
||||
youGotReply: "{name}からのリプライ"
|
||||
youGotQuote: "{name}による引用"
|
||||
youRenoted: "{name}がRenoteしました"
|
||||
youGotPoll: "{name}が投票しました"
|
||||
youGotMessagingMessageFromUser: "{name}からのチャットがあります"
|
||||
youGotMessagingMessageFromGroup: "{name}のチャットがあります"
|
||||
youWereFollowed: "フォローされました"
|
||||
@@ -1569,7 +1568,6 @@ _notification:
|
||||
renote: "Renote"
|
||||
quote: "引用"
|
||||
reaction: "リアクション"
|
||||
pollVote: "アンケートに投票された"
|
||||
pollEnded: "アンケートが終了"
|
||||
receiveFollowRequest: "フォロー申請を受け取った"
|
||||
followRequestAccepted: "フォローが受理された"
|
||||
|
||||
@@ -920,6 +920,10 @@ like: "좋아요!"
|
||||
unlike: "좋아요 취소"
|
||||
numberOfLikes: "좋아요 수"
|
||||
show: "표시"
|
||||
neverShow: "다시 보지 않기"
|
||||
remindMeLater: "나중에 알림"
|
||||
didYouLikeMisskey: "Misskey가 마음에 드시나요?"
|
||||
pleaseDonate: "{host}은(는) 무료 소프트웨어 Misskey를 사용합니다. 후원을 통해 저희의 개발이 이어질 수 있게 도와주세요!"
|
||||
_sensitiveMediaDetection:
|
||||
description: "기계학습을 통해 자동으로 민감한 미디어를 탐지하여, 모더레이션에 참고할 수 있도록 합니다. 서버의 부하를 약간 증가시킵니다."
|
||||
sensitivity: "탐지 민감도"
|
||||
|
||||
@@ -913,6 +913,10 @@ tools: "Nástroje"
|
||||
cannotLoad: "Nedá sa načítať."
|
||||
like: "Páči sa mi"
|
||||
show: "Zobraziť"
|
||||
neverShow: "Nabudúce nezobrazovať"
|
||||
remindMeLater: "Pripomenúť neskôr"
|
||||
didYouLikeMisskey: "Páči sa vám Misskey?"
|
||||
pleaseDonate: "Misskey je bezplatný softvér, ktorý používa {host}. Prosím, prispejte, aby sme ho mohli ďalej rozvíjať!"
|
||||
_sensitiveMediaDetection:
|
||||
description: "Strojové učenie sa použije na automatickú detekciu citlivých médií na účely ich moderovania. Mierne sa zvýši zaťaženie servera."
|
||||
sensitivity: "Citlivosť detekcie"
|
||||
|
||||
@@ -917,6 +917,8 @@ tools: "เครื่องมือ"
|
||||
cannotLoad: "ไม่สามารถโหลดได้"
|
||||
numberOfProfileView: "มุมมองโปรไฟล์"
|
||||
like: "ชื่นชอบ"
|
||||
unlike: "ไม่ชอบ"
|
||||
numberOfLikes: "จำนวนไลค์"
|
||||
show: "แสดงผล"
|
||||
_sensitiveMediaDetection:
|
||||
description: "ลดความพยายามในการดูแลเซิร์ฟเวอร์ผ่านการจดจำสื่อ NSFW โดยอัตโนมัติผ่านการเรียนรู้ของเครื่อง การทำสิ่งนี้อาจจะเพิ่มภาระบนเซิร์ฟเวอร์เล็กน้อย"
|
||||
@@ -1317,6 +1319,7 @@ _widgets:
|
||||
jobQueue: "คิวงาน"
|
||||
serverMetric: "ตัวชี้วัดเซิร์ฟเวอร์"
|
||||
aiscript: "AiScript คอนโซล"
|
||||
aiscriptApp: "AiScript แอพ"
|
||||
aichan: "เอไอ"
|
||||
userList: "รายชื่อผู้ใช้"
|
||||
_userList:
|
||||
@@ -1423,7 +1426,16 @@ _timelines:
|
||||
social: "โซเชี่ยล"
|
||||
global: "ทั่วโลก"
|
||||
_play:
|
||||
new: "สร้างการเล่น"
|
||||
edit: "แก้ไขเล่น"
|
||||
created: "สร้างการเล่นแล้ว"
|
||||
updated: "แก้ไขการเล่นแล้ว"
|
||||
deleted: "ลบการเล่นแล้ว"
|
||||
pageSetting: "ตั้งค่าการเล่น"
|
||||
editThisPage: "แก้ไข Play นี้"
|
||||
viewSource: "ดูต้นฉบับ"
|
||||
my: "มาย เพลย์"
|
||||
liked: "ไลค์ เพลย์"
|
||||
featured: "เป็นที่นิยม"
|
||||
title: "หัวข้อ"
|
||||
script: "สคริปต์"
|
||||
|
||||
@@ -918,7 +918,11 @@ cannotLoad: "無法載入"
|
||||
numberOfProfileView: "個人檔案檢視次數"
|
||||
like: "讚"
|
||||
unlike: "收回讚"
|
||||
numberOfLikes: "讚數"
|
||||
show: "檢視"
|
||||
neverShow: "不再顯示"
|
||||
remindMeLater: "以後再說"
|
||||
didYouLikeMisskey: "您是否喜愛Misskey呢?"
|
||||
_sensitiveMediaDetection:
|
||||
description: "您可以使用機器學習自動檢測敏感媒體並將其用於審核。 伺服器的負荷會稍微增加。"
|
||||
sensitivity: "檢測敏感度"
|
||||
@@ -1318,6 +1322,7 @@ _widgets:
|
||||
jobQueue: "佇列"
|
||||
serverMetric: "服務器指標 "
|
||||
aiscript: "AiScript控制台"
|
||||
aiscriptApp: "AiScript App"
|
||||
aichan: "小藍"
|
||||
userList: "使用者列表"
|
||||
_userList:
|
||||
@@ -1424,7 +1429,16 @@ _timelines:
|
||||
social: "社群"
|
||||
global: "公開"
|
||||
_play:
|
||||
new: "新增Play"
|
||||
edit: "編輯Play"
|
||||
created: "已新增Play"
|
||||
updated: "已更新Play"
|
||||
deleted: "已刪除Play"
|
||||
pageSetting: "Play設定"
|
||||
editThisPage: "編輯這個Play"
|
||||
viewSource: "檢視原始碼"
|
||||
my: "自己的Play"
|
||||
liked: "按了讚的Play"
|
||||
featured: "人氣"
|
||||
title: "標題"
|
||||
script: "腳本"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "misskey",
|
||||
"version": "13.0.0-beta.27",
|
||||
"version": "13.0.0-beta.30",
|
||||
"codename": "indigo",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -92,13 +92,6 @@ export class PollService {
|
||||
choice: choice,
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
// Notify
|
||||
this.createNotificationService.createNotification(note.userId, 'pollVote', {
|
||||
notifierId: user.id,
|
||||
noteId: note.id,
|
||||
choice: choice,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
||||
@@ -98,7 +98,7 @@ export class NotificationEntityService implements OnModuleInit {
|
||||
}),
|
||||
reaction: notification.reaction,
|
||||
} : {}),
|
||||
...(notification.type === 'pollVote' ? {
|
||||
...(notification.type === 'pollVote' ? { // TODO: そのうち消す
|
||||
note: this.noteEntityService.pack(notification.note ?? notification.noteId!, { id: notification.notifieeId }, {
|
||||
detail: true,
|
||||
_hint_: options._hintForEachNotes_,
|
||||
|
||||
@@ -55,11 +55,11 @@ export class Notification {
|
||||
* 通知の種類。
|
||||
* follow - フォローされた
|
||||
* mention - 投稿で自分が言及された
|
||||
* reply - (自分または自分がWatchしている)投稿が返信された
|
||||
* renote - (自分または自分がWatchしている)投稿がRenoteされた
|
||||
* quote - (自分または自分がWatchしている)投稿が引用Renoteされた
|
||||
* reaction - (自分または自分がWatchしている)投稿にリアクションされた
|
||||
* pollVote - (自分または自分がWatchしている)投稿のアンケートに投票された
|
||||
* reply - 投稿に返信された
|
||||
* renote - 投稿がRenoteされた
|
||||
* quote - 投稿が引用Renoteされた
|
||||
* reaction - 投稿にリアクションされた
|
||||
* pollVote - 投稿のアンケートに投票された (廃止)
|
||||
* pollEnded - 自分のアンケートもしくは自分が投票したアンケートが終了した
|
||||
* receiveFollowRequest - フォローリクエストされた
|
||||
* followRequestAccepted - 自分の送ったフォローリクエストが承認された
|
||||
|
||||
@@ -162,13 +162,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
userId: me.id,
|
||||
});
|
||||
|
||||
// Notify
|
||||
this.createNotificationService.createNotification(note.userId, 'pollVote', {
|
||||
notifierId: me.id,
|
||||
noteId: note.id,
|
||||
choice: ps.choice,
|
||||
});
|
||||
|
||||
// リモート投票の場合リプライ送信
|
||||
if (note.userHost != null) {
|
||||
const pollOwner = await this.usersRepository.findOneByOrFail({ id: note.userId }) as IRemoteUser;
|
||||
|
||||
@@ -74,7 +74,7 @@ function onMousedown(evt: Event) {
|
||||
}
|
||||
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1), transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
transform-origin: left top;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="ssazuxis">
|
||||
<header class="_button" :style="{ background: bg }" @click="showBody = !showBody">
|
||||
<div class="title"><slot name="header"></slot></div>
|
||||
<div class="title"><div><slot name="header"></slot></div></div>
|
||||
<div class="divider"></div>
|
||||
<button class="_button">
|
||||
<template v-if="showBody"><i class="ti ti-chevron-up"></i></template>
|
||||
@@ -127,14 +127,6 @@ export default defineComponent({
|
||||
place-content: center;
|
||||
margin: 0;
|
||||
padding: 12px 16px 12px 0;
|
||||
|
||||
> i {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> .divider {
|
||||
|
||||
@@ -251,17 +251,18 @@ onBeforeUnmount(() => {
|
||||
color: #fff;
|
||||
|
||||
&:before {
|
||||
background: #d42e2e;
|
||||
background: #d42e2e !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:active,
|
||||
&.active {
|
||||
color: var(--fgOnAccent);
|
||||
color: var(--fgOnAccent) !important;
|
||||
opacity: 1;
|
||||
|
||||
&:before {
|
||||
background: var(--accent);
|
||||
background: var(--accent) !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Transition :name="transitionName" :duration="transitionDuration" appear @after-leave="emit('closed')" @enter="emit('opening')" @after-enter="onOpened">
|
||||
<div v-show="manualShowing != null ? manualShowing : showing" v-hotkey.global="keymap" class="qzhlnise" :class="{ drawer: type === 'drawer', dialog: type === 'dialog' || type === 'dialog:top', popup: type === 'popup' }" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
|
||||
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
||||
<div class="bg _modalBg" :class="{ transparent: transparentBg && (type === 'popup') }" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
|
||||
<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
|
||||
<slot :max-height="maxHeight" :type="type"></slot>
|
||||
</div>
|
||||
@@ -63,6 +63,7 @@ let transformOrigin = $ref('center');
|
||||
let showing = $ref(true);
|
||||
let content = $shallowRef<HTMLElement>();
|
||||
const zIndex = os.claimZIndex(props.zPriority);
|
||||
let useSendAnime = $ref(false);
|
||||
const type = $computed<ModalTypes>(() => {
|
||||
if (props.preferType === 'auto') {
|
||||
if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
|
||||
@@ -74,15 +75,34 @@ const type = $computed<ModalTypes>(() => {
|
||||
return props.preferType!;
|
||||
}
|
||||
});
|
||||
let transitionName = $ref(defaultStore.state.animation ? (type === 'drawer') ? 'modal-drawer' : (type === 'popup') ? 'modal-popup' : 'modal' : '');
|
||||
let transitionDuration = $ref(defaultStore.state.animation ? 200 : 0);
|
||||
let transitionName = $computed((() =>
|
||||
defaultStore.state.animation
|
||||
? useSendAnime
|
||||
? 'send'
|
||||
: type === 'drawer'
|
||||
? 'modal-drawer'
|
||||
: type === 'popup'
|
||||
? 'modal-popup'
|
||||
: 'modal'
|
||||
: ''
|
||||
));
|
||||
let transitionDuration = $computed((() =>
|
||||
transitionName === 'send'
|
||||
? 400
|
||||
: transitionName === 'modal-popup'
|
||||
? 100
|
||||
: transitionName === 'modal'
|
||||
? 200
|
||||
: transitionName === 'modal-drawer'
|
||||
? 200
|
||||
: 0
|
||||
));
|
||||
|
||||
let contentClicking = false;
|
||||
|
||||
function close(opts: { useSendAnimation?: boolean } = {}) {
|
||||
if (opts.useSendAnimation) {
|
||||
transitionName = 'send';
|
||||
transitionDuration = 400;
|
||||
useSendAnime = true;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
@@ -308,12 +328,12 @@ defineExpose({
|
||||
|
||||
.modal-popup-enter-active, .modal-popup-leave-active {
|
||||
> .bg {
|
||||
transition: opacity 0.2s !important;
|
||||
transition: opacity 0.1s !important;
|
||||
}
|
||||
|
||||
> .content {
|
||||
transform-origin: var(--transformOrigin);
|
||||
transition: opacity 0.2s cubic-bezier(0, 0, 0.2, 1), transform 0.2s cubic-bezier(0, 0, 0.2, 1) !important;
|
||||
transition: opacity 0.1s cubic-bezier(0, 0, 0.2, 1), transform 0.1s cubic-bezier(0, 0, 0.2, 1) !important;
|
||||
}
|
||||
}
|
||||
.modal-popup-enter-from, .modal-popup-leave-to {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<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 class="avatar" :user="note.user"/>
|
||||
<MkAvatar v-once class="avatar" :user="note.user"/>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<I18n :src="i18n.ts.renotedBy" tag="span">
|
||||
<template #user>
|
||||
@@ -27,11 +27,16 @@
|
||||
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</button>
|
||||
<MkVisibility :note="note"/>
|
||||
<span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]">
|
||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<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 class="avatar" :user="appearNote.user"/>
|
||||
<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"/>
|
||||
@@ -44,7 +49,7 @@
|
||||
<div class="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>
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i"/>
|
||||
<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">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
@@ -75,14 +80,25 @@
|
||||
<i class="ti ti-arrow-back-up"></i>
|
||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
||||
</button>
|
||||
<MkRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
||||
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()">
|
||||
<button
|
||||
v-if="canRenote"
|
||||
ref="renoteButton"
|
||||
class="button _button"
|
||||
@mousedown="renote()"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p>
|
||||
</button>
|
||||
<button v-else class="button _button" disabled>
|
||||
<i class="ti ti-ban"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _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)">
|
||||
<i class="ti ti-minus"></i>
|
||||
</button>
|
||||
<button ref="menuButton" class="button _button" @click="menu()">
|
||||
<button ref="menuButton" class="button _button" @mousedown="menu()">
|
||||
<i class="ti ti-dots"></i>
|
||||
</button>
|
||||
</footer>
|
||||
@@ -111,10 +127,9 @@ import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
|
||||
import MkMediaList from '@/components/MkMediaList.vue';
|
||||
import MkCwButton from '@/components/MkCwButton.vue';
|
||||
import MkPoll from '@/components/MkPoll.vue';
|
||||
import MkRenoteButton from '@/components/MkRenoteButton.vue';
|
||||
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||
import MkVisibility from '@/components/MkVisibility.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login';
|
||||
import { focusPrev, focusNext } from '@/scripts/focus';
|
||||
import { checkWordMute } from '@/scripts/check-word-mute';
|
||||
@@ -128,6 +143,7 @@ import { i18n } from '@/i18n';
|
||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
@@ -158,7 +174,7 @@ const isRenote = (
|
||||
|
||||
const el = shallowRef<HTMLElement>();
|
||||
const menuButton = shallowRef<HTMLElement>();
|
||||
const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>();
|
||||
const renoteButton = shallowRef<HTMLElement>();
|
||||
const renoteTime = shallowRef<HTMLElement>();
|
||||
const reactButton = shallowRef<HTMLElement>();
|
||||
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
||||
@@ -175,6 +191,7 @@ const translation = ref(null);
|
||||
const translating = ref(false);
|
||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
|
||||
|
||||
const keymap = {
|
||||
'r': () => reply(true),
|
||||
@@ -193,6 +210,47 @@ useNoteCapture({
|
||||
isDeletedRef: isDeleted,
|
||||
});
|
||||
|
||||
useTooltip(renoteButton, async (showing) => {
|
||||
const renotes = await os.api('notes/renotes', {
|
||||
noteId: appearNote.id,
|
||||
limit: 11,
|
||||
});
|
||||
|
||||
const users = renotes.map(x => x.user);
|
||||
|
||||
if (users.length < 1) return;
|
||||
|
||||
os.popup(MkUsersTooltip, {
|
||||
showing,
|
||||
users,
|
||||
count: appearNote.renoteCount,
|
||||
targetElement: renoteButton.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
function renote(viaKeyboard = false) {
|
||||
pleaseLogin();
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.renote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
os.api('notes/create', {
|
||||
renoteId: appearNote.id,
|
||||
});
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.quote,
|
||||
icon: 'ti ti-quote',
|
||||
action: () => {
|
||||
os.post({
|
||||
renote: appearNote,
|
||||
});
|
||||
},
|
||||
}], renoteButton.value, {
|
||||
viaKeyboard,
|
||||
});
|
||||
}
|
||||
|
||||
function reply(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
os.post({
|
||||
|
||||
@@ -25,7 +25,12 @@
|
||||
<i v-if="isMyRenote" class="ti ti-dots dropdownIcon"></i>
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</button>
|
||||
<MkVisibility :note="note"/>
|
||||
<span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]">
|
||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<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">
|
||||
@@ -38,7 +43,12 @@
|
||||
</MkA>
|
||||
<span v-if="appearNote.user.isBot" class="is-bot">bot</span>
|
||||
<div class="info">
|
||||
<MkVisibility :note="appearNote"/>
|
||||
<span v-if="appearNote.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[appearNote.visibility]">
|
||||
<i v-if="appearNote.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="appearNote.visibility === 'followers'" class="ti ti-lock-open"></i>
|
||||
<i v-else-if="appearNote.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<span v-if="appearNote.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="username"><MkAcct :user="appearNote.user"/></div>
|
||||
@@ -85,14 +95,25 @@
|
||||
<i class="ti ti-arrow-back-up"></i>
|
||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
||||
</button>
|
||||
<MkRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
||||
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()">
|
||||
<button
|
||||
v-if="canRenote"
|
||||
ref="renoteButton"
|
||||
class="button _button"
|
||||
@mousedown="renote()"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="appearNote.renoteCount > 0" class="count">{{ appearNote.renoteCount }}</p>
|
||||
</button>
|
||||
<button v-else class="button _button" disabled>
|
||||
<i class="ti ti-ban"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _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)">
|
||||
<i class="ti ti-minus"></i>
|
||||
</button>
|
||||
<button ref="menuButton" class="button _button" @click="menu()">
|
||||
<button ref="menuButton" class="button _button" @mousedown="menu()">
|
||||
<i class="ti ti-dots"></i>
|
||||
</button>
|
||||
</footer>
|
||||
@@ -121,10 +142,9 @@ import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
|
||||
import MkMediaList from '@/components/MkMediaList.vue';
|
||||
import MkCwButton from '@/components/MkCwButton.vue';
|
||||
import MkPoll from '@/components/MkPoll.vue';
|
||||
import MkRenoteButton from '@/components/MkRenoteButton.vue';
|
||||
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
|
||||
import MkVisibility from '@/components/MkVisibility.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login';
|
||||
import { checkWordMute } from '@/scripts/check-word-mute';
|
||||
import { userPage } from '@/filters/user';
|
||||
@@ -138,6 +158,7 @@ import { i18n } from '@/i18n';
|
||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
@@ -168,7 +189,7 @@ const isRenote = (
|
||||
|
||||
const el = shallowRef<HTMLElement>();
|
||||
const menuButton = shallowRef<HTMLElement>();
|
||||
const renoteButton = shallowRef<InstanceType<typeof MkRenoteButton>>();
|
||||
const renoteButton = shallowRef<HTMLElement>();
|
||||
const renoteTime = shallowRef<HTMLElement>();
|
||||
const reactButton = shallowRef<HTMLElement>();
|
||||
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
||||
@@ -182,6 +203,7 @@ const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : n
|
||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||
const conversation = ref<misskey.entities.Note[]>([]);
|
||||
const replies = ref<misskey.entities.Note[]>([]);
|
||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
|
||||
|
||||
const keymap = {
|
||||
'r': () => reply(true),
|
||||
@@ -198,6 +220,47 @@ useNoteCapture({
|
||||
isDeletedRef: isDeleted,
|
||||
});
|
||||
|
||||
useTooltip(renoteButton, async (showing) => {
|
||||
const renotes = await os.api('notes/renotes', {
|
||||
noteId: appearNote.id,
|
||||
limit: 11,
|
||||
});
|
||||
|
||||
const users = renotes.map(x => x.user);
|
||||
|
||||
if (users.length < 1) return;
|
||||
|
||||
os.popup(MkUsersTooltip, {
|
||||
showing,
|
||||
users,
|
||||
count: appearNote.renoteCount,
|
||||
targetElement: renoteButton.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
function renote(viaKeyboard = false) {
|
||||
pleaseLogin();
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.renote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
os.api('notes/create', {
|
||||
renoteId: appearNote.id,
|
||||
});
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.quote,
|
||||
icon: 'ti ti-quote',
|
||||
action: () => {
|
||||
os.post({
|
||||
renote: appearNote,
|
||||
});
|
||||
},
|
||||
}], renoteButton.value, {
|
||||
viaKeyboard,
|
||||
});
|
||||
}
|
||||
|
||||
function reply(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
os.post({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<header class="kkwtjztg">
|
||||
<MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)">
|
||||
<MkA v-once v-user-preview="note.user.id" class="name" :to="userPage(note.user)">
|
||||
<MkUserName :user="note.user"/>
|
||||
</MkA>
|
||||
<div v-if="note.user.isBot" class="is-bot">bot</div>
|
||||
@@ -9,7 +9,12 @@
|
||||
<MkA class="created-at" :to="notePage(note)">
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</MkA>
|
||||
<MkVisibility :note="note"/>
|
||||
<span v-if="note.visibility !== 'public'" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility[note.visibility]">
|
||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<span v-if="note.localOnly" style="{ margin-left: 0.5em; }" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
@@ -17,7 +22,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkVisibility from '@/components/MkVisibility.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { notePage } from '@/filters/note';
|
||||
import { userPage } from '@/filters/user';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div ref="elRef" class="qglefbjs" :class="notification.type">
|
||||
<div class="head">
|
||||
<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=""/>
|
||||
@@ -13,10 +13,9 @@
|
||||
<i v-else-if="notification.type === 'reply'" class="ti ti-arrow-back-up"></i>
|
||||
<i v-else-if="notification.type === 'mention'" class="ti ti-at"></i>
|
||||
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
|
||||
<i v-else-if="notification.type === 'pollVote'" class="ti ti-chart-arrows"></i>
|
||||
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
|
||||
<!-- notification.reaction が null になることはまずないが、ここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
|
||||
<XReactionIcon
|
||||
<MkReactionIcon
|
||||
v-else-if="notification.type === 'reaction'"
|
||||
ref="reactionRef"
|
||||
:reaction="notification.reaction ? notification.reaction.replace(/^:(\w+):$/, ':$1@.:') : notification.reaction"
|
||||
@@ -32,50 +31,47 @@
|
||||
<span v-else>{{ notification.header }}</span>
|
||||
<MkTime v-if="withTime" :time="notification.createdAt" class="time"/>
|
||||
</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>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||
<i class="ti ti-quote"></i>
|
||||
</MkA>
|
||||
<MkA v-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
||||
<MkA v-else-if="notification.type === 'renote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note.renote)">
|
||||
<i class="ti ti-quote"></i>
|
||||
<Mfm :text="getNoteSummary(notification.note.renote)" :plain="true" :nowrap="!full"/>
|
||||
<i class="ti ti-quote"></i>
|
||||
</MkA>
|
||||
<MkA v-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<MkA v-else-if="notification.type === 'reply'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||
</MkA>
|
||||
<MkA v-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<MkA v-else-if="notification.type === 'mention'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||
</MkA>
|
||||
<MkA v-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<MkA v-else-if="notification.type === 'quote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||
</MkA>
|
||||
<MkA v-if="notification.type === 'pollVote'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<MkA v-else-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<i class="ti ti-quote"></i>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||
<i class="ti ti-quote"></i>
|
||||
</MkA>
|
||||
<MkA v-if="notification.type === 'pollEnded'" class="text" :to="notePage(notification.note)" :title="getNoteSummary(notification.note)">
|
||||
<i class="ti ti-quote"></i>
|
||||
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="!full"/>
|
||||
<i class="ti ti-quote"></i>
|
||||
</MkA>
|
||||
<span v-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-if="notification.type === 'followRequestAccepted'" class="text" style="opacity: 0.6;">{{ i18n.ts.followRequestAccepted }}</span>
|
||||
<span v-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-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-if="notification.type === 'app'" class="text">
|
||||
<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">
|
||||
<Mfm :text="notification.body" :nowrap="!full"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, onMounted, onUnmounted, watch } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import MkFollowButton from '@/components/MkFollowButton.vue';
|
||||
import XReactionTooltip from '@/components/MkReactionTooltip.vue';
|
||||
import { getNoteSummary } from '@/scripts/get-note-summary';
|
||||
@@ -239,12 +235,6 @@ useTooltip(reactionRef, (showing) => {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.pollVote {
|
||||
padding: 3px;
|
||||
background: #88a6b7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.pollEnded {
|
||||
padding: 3px;
|
||||
background: #88a6b7;
|
||||
@@ -275,6 +265,7 @@ useTooltip(reactionRef, (showing) => {
|
||||
}
|
||||
}
|
||||
|
||||
> .content {
|
||||
> .text {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
@@ -295,6 +286,7 @@ useTooltip(reactionRef, (showing) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@container (max-width: 600px) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
|
||||
<span class="text" :class="{ up }">
|
||||
<XReactionIcon class="icon" :reaction="reaction"/>
|
||||
<MkReactionIcon class="icon" :reaction="reaction"/>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -9,7 +9,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import * as os from '@/os';
|
||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
reaction: string;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
|
||||
<div class="beeadbfb">
|
||||
<XReactionIcon :reaction="reaction" class="icon" :no-style="true"/>
|
||||
<MkReactionIcon :reaction="reaction" class="icon" :no-style="true"/>
|
||||
<div class="name">{{ reaction.replace('@.', '') }}</div>
|
||||
</div>
|
||||
</MkTooltip>
|
||||
@@ -10,7 +10,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkTooltip from './MkTooltip.vue';
|
||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
|
||||
defineProps<{
|
||||
showing: boolean;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="340" @closed="emit('closed')">
|
||||
<div class="bqxuuuey">
|
||||
<div class="reaction">
|
||||
<XReactionIcon :reaction="reaction" class="icon" :no-style="true"/>
|
||||
<MkReactionIcon :reaction="reaction" class="icon" :no-style="true"/>
|
||||
<div class="name">{{ getReactionName(reaction) }}</div>
|
||||
</div>
|
||||
<div class="users">
|
||||
@@ -19,7 +19,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import MkTooltip from './MkTooltip.vue';
|
||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import { getEmojiName } from '@/scripts/emojilist';
|
||||
|
||||
defineProps<{
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<button
|
||||
ref="buttonRef"
|
||||
ref="buttonEl"
|
||||
v-ripple="canToggle"
|
||||
class="hkzvhatu _button"
|
||||
:class="{ reacted: note.myReaction == reaction, canToggle }"
|
||||
@click="toggleReaction()"
|
||||
>
|
||||
<XReactionIcon class="icon" :reaction="reaction"/>
|
||||
<MkReactionIcon class="icon" :reaction="reaction"/>
|
||||
<span class="count">{{ count }}</span>
|
||||
</button>
|
||||
</template>
|
||||
@@ -15,7 +15,7 @@
|
||||
import { computed, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import MkReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
import * as os from '@/os';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
import { $i } from '@/account';
|
||||
@@ -28,7 +28,7 @@ const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
}>();
|
||||
|
||||
const buttonRef = shallowRef<HTMLElement>();
|
||||
const buttonEl = shallowRef<HTMLElement>();
|
||||
|
||||
const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
|
||||
|
||||
@@ -58,9 +58,9 @@ const toggleReaction = () => {
|
||||
const anime = () => {
|
||||
if (document.hidden) return;
|
||||
|
||||
const rect = buttonRef.value.getBoundingClientRect();
|
||||
const x = rect.left + (buttonRef.value.offsetWidth / 2);
|
||||
const y = rect.top + (buttonRef.value.offsetHeight / 2);
|
||||
const rect = buttonEl.value.getBoundingClientRect();
|
||||
const x = rect.left + 16;
|
||||
const y = rect.top + (buttonEl.value.offsetHeight / 2);
|
||||
os.popup(MkPlusOneEffect, { reaction: props.reaction, x, y }, {}, 'end');
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@ onMounted(() => {
|
||||
if (!props.isInitial) anime();
|
||||
});
|
||||
|
||||
useTooltip(buttonRef, async (showing) => {
|
||||
useTooltip(buttonEl, async (showing) => {
|
||||
const reactions = await os.apiGet('notes/reactions', {
|
||||
noteId: props.note.id,
|
||||
type: props.reaction,
|
||||
@@ -87,7 +87,7 @@ useTooltip(buttonRef, async (showing) => {
|
||||
reaction: props.reaction,
|
||||
users,
|
||||
count: props.count,
|
||||
targetElement: buttonRef.value,
|
||||
targetElement: buttonEl.value,
|
||||
}, {}, 'closed');
|
||||
}, 100);
|
||||
</script>
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
<template>
|
||||
<button
|
||||
v-if="canRenote"
|
||||
ref="buttonRef"
|
||||
class="eddddedb _button canRenote"
|
||||
@click="renote()"
|
||||
>
|
||||
<i class="ti ti-repeat"></i>
|
||||
<p v-if="count > 0" class="count">{{ count }}</p>
|
||||
</button>
|
||||
<button v-else class="eddddedb _button">
|
||||
<i class="ti ti-ban"></i>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, shallowRef } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import XDetails from '@/components/MkUsersTooltip.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login';
|
||||
import * as os from '@/os';
|
||||
import { $i } from '@/account';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
count: number;
|
||||
}>();
|
||||
|
||||
const buttonRef = shallowRef<HTMLElement>();
|
||||
|
||||
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
|
||||
|
||||
useTooltip(buttonRef, async (showing) => {
|
||||
const renotes = await os.api('notes/renotes', {
|
||||
noteId: props.note.id,
|
||||
limit: 11,
|
||||
});
|
||||
|
||||
const users = renotes.map(x => x.user);
|
||||
|
||||
if (users.length < 1) return;
|
||||
|
||||
os.popup(XDetails, {
|
||||
showing,
|
||||
users,
|
||||
count: props.count,
|
||||
targetElement: buttonRef.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
|
||||
const renote = (viaKeyboard = false) => {
|
||||
pleaseLogin();
|
||||
os.popupMenu([{
|
||||
text: i18n.ts.renote,
|
||||
icon: 'ti ti-repeat',
|
||||
action: () => {
|
||||
os.api('notes/create', {
|
||||
renoteId: props.note.id,
|
||||
});
|
||||
},
|
||||
}, {
|
||||
text: i18n.ts.quote,
|
||||
icon: 'ti ti-quote',
|
||||
action: () => {
|
||||
os.post({
|
||||
renote: props.note,
|
||||
});
|
||||
},
|
||||
}], buttonRef.value, {
|
||||
viaKeyboard,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.eddddedb {
|
||||
display: inline-block;
|
||||
height: 32px;
|
||||
margin: 2px;
|
||||
padding: 0 6px;
|
||||
border-radius: 4px;
|
||||
|
||||
&:not(.canRenote) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
&.renoted {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
> .count {
|
||||
display: inline;
|
||||
margin-left: 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="vblkjoeq">
|
||||
<div class="label" @click="focus"><slot name="label"></slot></div>
|
||||
<div ref="container" class="input" :class="{ inline, disabled, focused }" @click.prevent="onClick">
|
||||
<div ref="container" class="input" :class="{ inline, disabled, focused }" @mousedown.prevent="show">
|
||||
<div ref="prefixEl" class="prefix"><slot name="prefix"></slot></div>
|
||||
<select
|
||||
ref="inputEl"
|
||||
@@ -118,7 +118,7 @@ onMounted(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const onClick = (ev: MouseEvent) => {
|
||||
function show(ev: MouseEvent) {
|
||||
focused.value = true;
|
||||
opening.value = true;
|
||||
|
||||
@@ -166,7 +166,7 @@ const onClick = (ev: MouseEvent) => {
|
||||
}).then(() => {
|
||||
focused.value = false;
|
||||
});
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -285,7 +285,7 @@ const onClick = (ev: MouseEvent) => {
|
||||
|
||||
<style lang="scss" module>
|
||||
.chevron {
|
||||
transition: transform 0.5s ease;
|
||||
transition: transform 0.1s ease-out;
|
||||
}
|
||||
|
||||
.chevronOpening {
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
|
||||
</MkInput>
|
||||
<MkButton type="submit" primary :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
||||
<MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
||||
</div>
|
||||
<div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
|
||||
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
|
||||
@@ -36,7 +36,7 @@
|
||||
<template #label>{{ i18n.ts.token }}</template>
|
||||
<template #prefix><i class="ti ti-123"></i></template>
|
||||
</MkInput>
|
||||
<MkButton type="submit" :disabled="signing" primary style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
||||
<MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<span v-if="note.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
|
||||
<span v-if="note.deletedAt" style="opacity: 0.5">({{ i18n.ts.deleted }})</span>
|
||||
<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
|
||||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i"/>
|
||||
<Mfm v-if="note.text" v-once :text="note.text" :author="note.user" :i="$i"/>
|
||||
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
|
||||
</div>
|
||||
<details v-if="note.files.length > 0">
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<template>
|
||||
<span v-if="note.visibility !== 'public'" :class="$style.visibility" :title="i18n.ts._visibility[note.visibility]">
|
||||
<i v-if="note.visibility === 'home'" class="ti ti-home"></i>
|
||||
<i v-else-if="note.visibility === 'followers'" class="ti ti-lock-open"></i>
|
||||
<i v-else-if="note.visibility === 'specified'" ref="specified" class="ti ti-mail"></i>
|
||||
</span>
|
||||
<span v-if="note.localOnly" :class="$style.localOnly" :title="i18n.ts._visibility['localOnly']"><i class="ti ti-world-off"></i></span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue';
|
||||
import XDetails from '@/components/MkUsersTooltip.vue';
|
||||
import * as os from '@/os';
|
||||
import { useTooltip } from '@/scripts/use-tooltip';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
note: {
|
||||
visibility: string;
|
||||
localOnly?: boolean;
|
||||
visibleUserIds?: string[];
|
||||
},
|
||||
}>();
|
||||
|
||||
const specified = $shallowRef<HTMLElement>();
|
||||
|
||||
if (props.note.visibility === 'specified') {
|
||||
useTooltip($$(specified), async (showing) => {
|
||||
const users = await os.api('users/show', {
|
||||
userIds: props.note.visibleUserIds,
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
os.popup(XDetails, {
|
||||
showing,
|
||||
users,
|
||||
count: props.note.visibleUserIds.length,
|
||||
targetElement: specified,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.visibility, .localOnly {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
</style>
|
||||
@@ -22,7 +22,7 @@
|
||||
<MkFolder v-for="category in customEmojiCategories" :key="category" class="emojis">
|
||||
<template #header>{{ category || $ts.other }}</template>
|
||||
<div class="zuvgdzyt">
|
||||
<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" :key="emoji.name" class="emoji" :emoji="emoji"/>
|
||||
<XEmoji v-for="emoji in customEmojis.filter(e => e.category === category)" v-once :key="emoji.name" class="emoji" :emoji="emoji"/>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<MkPagination v-slot="{items}" ref="instances" :key="host + state" :pagination="pagination">
|
||||
<div class="dqokceoi">
|
||||
<MkA v-for="instance in items" :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`">
|
||||
<MkA v-for="instance in items" v-once :key="instance.id" v-tooltip.mfm="`Status: ${getStatus(instance)}`" class="instance" :to="`/instance-info/${instance.host}`">
|
||||
<MkInstanceCardMini :instance="instance"/>
|
||||
</MkA>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
|
||||
<MkPagination v-slot="{items}" ref="paginationComponent" :pagination="pagination" class="users">
|
||||
<MkA v-for="user in items" :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/user-info/${user.id}`">
|
||||
<MkA v-for="user in items" v-once :key="user.id" v-tooltip.mfm="`Last posted: ${dateString(user.updatedAt)}`" class="user" :to="`/user-info/${user.id}`">
|
||||
<MkUserCardMini :user="user"/>
|
||||
</MkA>
|
||||
</MkPagination>
|
||||
|
||||
@@ -53,14 +53,14 @@ definePageMetadata({
|
||||
<style lang="scss" scoped>
|
||||
.ruryvtyk {
|
||||
> .announcement {
|
||||
> .header {
|
||||
padding: 16px;
|
||||
|
||||
> .header {
|
||||
margin-bottom: 16px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
> .content {
|
||||
padding: 0 16px;
|
||||
|
||||
> img {
|
||||
display: block;
|
||||
max-height: 300px;
|
||||
@@ -69,7 +69,7 @@ definePageMetadata({
|
||||
}
|
||||
|
||||
> .footer {
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ const headerActions = $computed(() => [{
|
||||
const headerTabs = $computed(() => [{
|
||||
key: 'featured',
|
||||
title: i18n.ts._play.featured,
|
||||
icon: 'fas fa-fire-alt',
|
||||
icon: 'ti ti-flare',
|
||||
}, {
|
||||
key: 'my',
|
||||
title: i18n.ts._play.my,
|
||||
|
||||
@@ -63,7 +63,7 @@ const headerActions = $computed(() => [{
|
||||
const headerTabs = $computed(() => [{
|
||||
key: 'featured',
|
||||
title: i18n.ts._pages.featured,
|
||||
icon: 'fas fa-fire-alt',
|
||||
icon: 'ti ti-flare',
|
||||
}, {
|
||||
key: 'my',
|
||||
title: i18n.ts._pages.my,
|
||||
|
||||
@@ -12,12 +12,37 @@ import { computed } from 'vue';
|
||||
import XNotes from '@/components/MkNotes.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import * as os from '@/os';
|
||||
import { useRouter } from '@/router';
|
||||
import { $i } from '@/account';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
query: string;
|
||||
channel?: string;
|
||||
}>();
|
||||
|
||||
const query = props.query;
|
||||
|
||||
if ($i != null) {
|
||||
if (query.startsWith('https://') || (query.startsWith('@') && !query.includes(' '))) {
|
||||
const promise = os.api('ap/show', {
|
||||
uri: props.query,
|
||||
});
|
||||
|
||||
os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject);
|
||||
|
||||
const res = await promise;
|
||||
|
||||
if (res.type === 'User') {
|
||||
router.replace(`/@${res.object.username}@${res.object.host}`);
|
||||
} else if (res.type === 'Note') {
|
||||
router.replace(`/notes/${res.object.id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pagination = {
|
||||
endpoint: 'notes/search' as const,
|
||||
limit: 10,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="_gaps_m">
|
||||
<div class="">
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps">
|
||||
<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton>
|
||||
|
||||
<div v-for="account in accounts" :key="account.id" class="_panel _button lcjjdxlm" @click="menu(account, $event)">
|
||||
@@ -16,6 +17,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormSuspense>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
174
packages/frontend/src/pages/user/activity.following.vue
Normal file
174
packages/frontend/src/pages/user/activity.following.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div>
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div v-show="!fetching" :class="$style.root" class="_panel">
|
||||
<canvas ref="chartEl"></canvas>
|
||||
<MkChartLegend ref="legendEl" style="margin-top: 8px;"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { Chart, ChartDataset } from 'chart.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import * as misskey from 'misskey-js';
|
||||
import gradient from 'chartjs-plugin-gradient';
|
||||
import { satisfies } from 'compare-versions';
|
||||
import * as os from '@/os';
|
||||
import { defaultStore } from '@/store';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||
import { chartVLine } from '@/scripts/chart-vline';
|
||||
import { alpha } from '@/scripts/color';
|
||||
import { initChart } from '@/scripts/init-chart';
|
||||
import { chartLegend } from '@/scripts/chart-legend';
|
||||
import MkChartLegend from '@/components/MkChartLegend.vue';
|
||||
|
||||
initChart();
|
||||
|
||||
const props = defineProps<{
|
||||
user: misskey.entities.User;
|
||||
}>();
|
||||
|
||||
const chartEl = $shallowRef<HTMLCanvasElement>(null);
|
||||
let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>();
|
||||
const now = new Date();
|
||||
let chartInstance: Chart = null;
|
||||
const chartLimit = 50;
|
||||
let fetching = $ref(true);
|
||||
|
||||
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||
|
||||
async function renderChart() {
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy();
|
||||
}
|
||||
|
||||
const getDate = (ago: number) => {
|
||||
const y = now.getFullYear();
|
||||
const m = now.getMonth();
|
||||
const d = now.getDate();
|
||||
|
||||
return new Date(y, m, d - ago);
|
||||
};
|
||||
|
||||
const format = (arr) => {
|
||||
return arr.map((v, i) => ({
|
||||
x: getDate(i).getTime(),
|
||||
y: v,
|
||||
}));
|
||||
};
|
||||
|
||||
const raw = await os.api('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' });
|
||||
|
||||
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
|
||||
const colorFollowLocal = '#008FFB';
|
||||
const colorFollowRemote = '#008FFB88';
|
||||
const colorFollowedLocal = '#2ecc71';
|
||||
const colorFollowedRemote = '#2ecc7188';
|
||||
|
||||
function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset {
|
||||
return Object.assign({
|
||||
label: label,
|
||||
data: data,
|
||||
parsing: false,
|
||||
pointRadius: 0,
|
||||
borderWidth: 0,
|
||||
borderJoinStyle: 'round',
|
||||
borderRadius: 4,
|
||||
barPercentage: 0.9,
|
||||
fill: true,
|
||||
} satisfies ChartDataset, extra);
|
||||
}
|
||||
|
||||
chartInstance = new Chart(chartEl, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
datasets: [
|
||||
makeDataset('Follow (local)', format(raw.local.followings.inc).slice().reverse(), { backgroundColor: colorFollowLocal }),
|
||||
makeDataset('Follow (remote)', format(raw.remote.followings.inc).slice().reverse(), { backgroundColor: colorFollowRemote }),
|
||||
makeDataset('Followed (local)', format(raw.local.followers.inc).slice().reverse(), { backgroundColor: colorFollowedLocal }),
|
||||
makeDataset('Followed (remote)', format(raw.remote.followers.inc).slice().reverse(), { backgroundColor: colorFollowedRemote }),
|
||||
],
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 3,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 8,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
offset: true,
|
||||
stacked: true,
|
||||
time: {
|
||||
stepSize: 1,
|
||||
unit: 'day',
|
||||
displayFormats: {
|
||||
day: 'M/d',
|
||||
month: 'Y/M',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
display: true,
|
||||
maxRotation: 0,
|
||||
autoSkipPadding: 8,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
position: 'left',
|
||||
stacked: true,
|
||||
suggestedMax: 10,
|
||||
grid: {
|
||||
display: true,
|
||||
},
|
||||
ticks: {
|
||||
display: true,
|
||||
//mirror: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
mode: 'index',
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
external: externalTooltipHandler,
|
||||
},
|
||||
gradient,
|
||||
},
|
||||
},
|
||||
plugins: [chartVLine(vLineColor), chartLegend(legendEl)],
|
||||
});
|
||||
|
||||
fetching = false;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
renderChart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
174
packages/frontend/src/pages/user/activity.notes.vue
Normal file
174
packages/frontend/src/pages/user/activity.notes.vue
Normal file
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div>
|
||||
<MkLoading v-if="fetching"/>
|
||||
<div v-show="!fetching" :class="$style.root" class="_panel">
|
||||
<canvas ref="chartEl"></canvas>
|
||||
<MkChartLegend ref="legendEl" style="margin-top: 8px;"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { Chart, ChartDataset } from 'chart.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import * as misskey from 'misskey-js';
|
||||
import gradient from 'chartjs-plugin-gradient';
|
||||
import { satisfies } from 'compare-versions';
|
||||
import * as os from '@/os';
|
||||
import { defaultStore } from '@/store';
|
||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip';
|
||||
import { chartVLine } from '@/scripts/chart-vline';
|
||||
import { alpha } from '@/scripts/color';
|
||||
import { initChart } from '@/scripts/init-chart';
|
||||
import { chartLegend } from '@/scripts/chart-legend';
|
||||
import MkChartLegend from '@/components/MkChartLegend.vue';
|
||||
|
||||
initChart();
|
||||
|
||||
const props = defineProps<{
|
||||
user: misskey.entities.User;
|
||||
}>();
|
||||
|
||||
const chartEl = $shallowRef<HTMLCanvasElement>(null);
|
||||
let legendEl = $shallowRef<InstanceType<typeof MkChartLegend>>();
|
||||
const now = new Date();
|
||||
let chartInstance: Chart = null;
|
||||
const chartLimit = 50;
|
||||
let fetching = $ref(true);
|
||||
|
||||
const { handler: externalTooltipHandler } = useChartTooltip();
|
||||
|
||||
async function renderChart() {
|
||||
if (chartInstance) {
|
||||
chartInstance.destroy();
|
||||
}
|
||||
|
||||
const getDate = (ago: number) => {
|
||||
const y = now.getFullYear();
|
||||
const m = now.getMonth();
|
||||
const d = now.getDate();
|
||||
|
||||
return new Date(y, m, d - ago);
|
||||
};
|
||||
|
||||
const format = (arr) => {
|
||||
return arr.map((v, i) => ({
|
||||
x: getDate(i).getTime(),
|
||||
y: v,
|
||||
}));
|
||||
};
|
||||
|
||||
const raw = await os.api('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
|
||||
|
||||
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
|
||||
|
||||
const colorNormal = '#008FFB';
|
||||
const colorReply = '#FEB019';
|
||||
const colorRenote = '#00E396';
|
||||
const colorFile = '#e300db';
|
||||
|
||||
function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset {
|
||||
return Object.assign({
|
||||
label: label,
|
||||
data: data,
|
||||
parsing: false,
|
||||
pointRadius: 0,
|
||||
borderWidth: 0,
|
||||
borderJoinStyle: 'round',
|
||||
borderRadius: 4,
|
||||
barPercentage: 0.9,
|
||||
fill: true,
|
||||
} satisfies ChartDataset, extra);
|
||||
}
|
||||
|
||||
chartInstance = new Chart(chartEl, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
datasets: [
|
||||
makeDataset('Normal', format(raw.diffs.normal).slice().reverse(), { backgroundColor: colorNormal }),
|
||||
makeDataset('Reply', format(raw.diffs.reply).slice().reverse(), { backgroundColor: colorReply }),
|
||||
makeDataset('Renote', format(raw.diffs.renote).slice().reverse(), { backgroundColor: colorRenote }),
|
||||
makeDataset('File', format(raw.diffs.withFile).slice().reverse(), { backgroundColor: colorFile }),
|
||||
],
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 3,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
right: 8,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
offset: true,
|
||||
stacked: true,
|
||||
time: {
|
||||
stepSize: 1,
|
||||
unit: 'day',
|
||||
displayFormats: {
|
||||
day: 'M/d',
|
||||
month: 'Y/M',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
ticks: {
|
||||
display: true,
|
||||
maxRotation: 0,
|
||||
autoSkipPadding: 8,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
position: 'left',
|
||||
stacked: true,
|
||||
suggestedMax: 10,
|
||||
grid: {
|
||||
display: true,
|
||||
},
|
||||
ticks: {
|
||||
display: true,
|
||||
//mirror: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltip: {
|
||||
enabled: false,
|
||||
mode: 'index',
|
||||
animation: {
|
||||
duration: 0,
|
||||
},
|
||||
external: externalTooltipHandler,
|
||||
},
|
||||
gradient,
|
||||
},
|
||||
},
|
||||
plugins: [chartVLine(vLineColor), chartLegend(legendEl)],
|
||||
});
|
||||
|
||||
fetching = false;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
renderChart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { markRaw, version as vueVersion, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { Chart } from 'chart.js';
|
||||
import { Chart, ChartDataset } from 'chart.js';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import * as misskey from 'misskey-js';
|
||||
import gradient from 'chartjs-plugin-gradient';
|
||||
@@ -67,65 +67,33 @@ async function renderChart() {
|
||||
const colorUser2 = '#3498db88';
|
||||
const colorVisitor2 = '#2ecc7188';
|
||||
|
||||
function makeDataset(label: string, data: ChartDataset['data'], extra: Partial<ChartDataset> = {}): ChartDataset {
|
||||
return Object.assign({
|
||||
label: label,
|
||||
data: data,
|
||||
parsing: false,
|
||||
pointRadius: 0,
|
||||
borderWidth: 0,
|
||||
borderJoinStyle: 'round',
|
||||
borderRadius: 4,
|
||||
barPercentage: 0.7,
|
||||
categoryPercentage: 0.7,
|
||||
fill: true,
|
||||
} satisfies ChartDataset, extra);
|
||||
}
|
||||
|
||||
chartInstance = new Chart(chartEl, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
datasets: [{
|
||||
parsing: false,
|
||||
label: 'UPV (user)',
|
||||
data: format(raw.upv.user).slice().reverse(),
|
||||
pointRadius: 0,
|
||||
borderWidth: 0,
|
||||
borderJoinStyle: 'round',
|
||||
borderRadius: 4,
|
||||
backgroundColor: colorUser,
|
||||
barPercentage: 0.7,
|
||||
categoryPercentage: 0.7,
|
||||
fill: true,
|
||||
stack: 'u',
|
||||
}, {
|
||||
parsing: false,
|
||||
label: 'UPV (visitor)',
|
||||
data: format(raw.upv.visitor).slice().reverse(),
|
||||
pointRadius: 0,
|
||||
borderWidth: 0,
|
||||
borderJoinStyle: 'round',
|
||||
borderRadius: 4,
|
||||
backgroundColor: colorVisitor,
|
||||
barPercentage: 0.7,
|
||||
categoryPercentage: 0.7,
|
||||
fill: true,
|
||||
stack: 'u',
|
||||
}, {
|
||||
parsing: false,
|
||||
label: 'NPV (user)',
|
||||
data: format(raw.pv.user).slice().reverse(),
|
||||
pointRadius: 0,
|
||||
borderWidth: 0,
|
||||
borderJoinStyle: 'round',
|
||||
borderRadius: 4,
|
||||
backgroundColor: colorUser2,
|
||||
barPercentage: 0.7,
|
||||
categoryPercentage: 0.7,
|
||||
fill: true,
|
||||
stack: 'n',
|
||||
}, {
|
||||
parsing: false,
|
||||
label: 'NPV (visitor)',
|
||||
data: format(raw.pv.visitor).slice().reverse(),
|
||||
pointRadius: 0,
|
||||
borderWidth: 0,
|
||||
borderJoinStyle: 'round',
|
||||
borderRadius: 4,
|
||||
backgroundColor: colorVisitor2,
|
||||
barPercentage: 0.7,
|
||||
categoryPercentage: 0.7,
|
||||
fill: true,
|
||||
stack: 'n',
|
||||
}],
|
||||
datasets: [
|
||||
makeDataset('UPV (user)', format(raw.upv.user).slice().reverse(), { backgroundColor: colorUser, stack: 'u' }),
|
||||
makeDataset('UPV (visitor)', format(raw.upv.visitor).slice().reverse(), { backgroundColor: colorVisitor, stack: 'u' }),
|
||||
makeDataset('NPV (user)', format(raw.pv.user).slice().reverse(), { backgroundColor: colorUser2, stack: 'n' }),
|
||||
makeDataset('UPV (visitor)', format(raw.pv.visitor).slice().reverse(), { backgroundColor: colorVisitor2, stack: 'n' }),
|
||||
],
|
||||
},
|
||||
options: {
|
||||
aspectRatio: 2.5,
|
||||
aspectRatio: 3,
|
||||
layout: {
|
||||
padding: {
|
||||
left: 0,
|
||||
|
||||
@@ -2,11 +2,19 @@
|
||||
<MkSpacer :content-max="700">
|
||||
<div class="_gaps">
|
||||
<MkFolder class="item">
|
||||
<template #header>Heatmap</template>
|
||||
<template #header><i class="ti ti-activity"></i> Heatmap</template>
|
||||
<XHeatmap :user="user" :src="'notes'"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="item">
|
||||
<template #header>PV</template>
|
||||
<template #header><i class="ti ti-pencil"></i> Notes</template>
|
||||
<XNotes :user="user"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="item">
|
||||
<template #header><i class="ti ti-users"></i> Following</template>
|
||||
<XFollowing :user="user"/>
|
||||
</MkFolder>
|
||||
<MkFolder class="item">
|
||||
<template #header><i class="ti ti-eye"></i> PV</template>
|
||||
<XPv :user="user"/>
|
||||
</MkFolder>
|
||||
</div>
|
||||
@@ -18,6 +26,8 @@ import { computed } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import XHeatmap from './activity.heatmap.vue';
|
||||
import XPv from './activity.pv.vue';
|
||||
import XNotes from './activity.notes.vue';
|
||||
import XFollowing from './activity.following.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
|
||||
@@ -24,6 +24,7 @@ export const getBuiltinThemes = () => Promise.all(
|
||||
'l-coffee',
|
||||
'l-apricot',
|
||||
'l-rainy',
|
||||
'l-botanical',
|
||||
'l-vivid',
|
||||
'l-cherry',
|
||||
'l-sushi',
|
||||
|
||||
29
packages/frontend/src/themes/l-botanical.json5
Normal file
29
packages/frontend/src/themes/l-botanical.json5
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
id: '1100673c-f902-4ccd-93aa-7cb88be56178',
|
||||
|
||||
name: 'Mi Botanical Light',
|
||||
author: 'ThinaticSystem',
|
||||
|
||||
base: 'light',
|
||||
|
||||
props: {
|
||||
accent: '#77b58c',
|
||||
bg: 'e2deda',
|
||||
fg: '#3d3d3d',
|
||||
fgHighlighted: '#6bc9a0',
|
||||
divider: '#cfcfcf',
|
||||
panel: '@X14',
|
||||
panelHeaderBg: '@panel',
|
||||
panelHeaderDivider: '@divider',
|
||||
header: ':alpha<0.7<@panel',
|
||||
navBg: '@X14',
|
||||
renote: '#229e92',
|
||||
mention: '#da6d35',
|
||||
mentionMe: '#d44c4c',
|
||||
hashtag: '#4cb8d4',
|
||||
link: '@accent',
|
||||
buttonGradateB: ':hue<-70<@accent',
|
||||
success: '#86b300',
|
||||
X14: '#ebe7e5'
|
||||
},
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
<div v-if="showMenu" class="menu">
|
||||
<MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA>
|
||||
<MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA>
|
||||
<MkA to="/featured" class="link" active-class="active"><i class="fas fa-fire-alt icon"></i>{{ $ts.featured }}</MkA>
|
||||
<MkA to="/featured" class="link" active-class="active"><i class="ti ti-flare icon"></i>{{ $ts.featured }}</MkA>
|
||||
<MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA>
|
||||
<div class="action">
|
||||
<button class="_buttonPrimary" @click="signup()">{{ $ts.signup }}</button>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="content">
|
||||
<MkA to="/" class="link" active-class="active"><i class="ti ti-home icon"></i>{{ $ts.home }}</MkA>
|
||||
<MkA to="/explore" class="link" active-class="active"><i class="ti ti-hash icon"></i>{{ $ts.explore }}</MkA>
|
||||
<MkA to="/featured" class="link" active-class="active"><i class="fas fa-fire-alt icon"></i>{{ $ts.featured }}</MkA>
|
||||
<MkA to="/featured" class="link" active-class="active"><i class="ti ti-flare icon"></i>{{ $ts.featured }}</MkA>
|
||||
<MkA to="/channels" class="link" active-class="active"><i class="ti ti-device-tv icon"></i>{{ $ts.channel }}</MkA>
|
||||
<div v-if="info" class="page active link">
|
||||
<div class="title">
|
||||
|
||||
@@ -11,19 +11,19 @@
|
||||
</div>
|
||||
<div class="info">
|
||||
<div>
|
||||
<p>{{ i18n.ts.today }}: <b>{{ dayP.toFixed(1) }}%</b></p>
|
||||
<p>{{ i18n.ts.today }}<b>{{ dayP.toFixed(1) }}%</b></p>
|
||||
<div class="meter">
|
||||
<div class="val" :style="{ width: `${dayP}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ i18n.ts.thisMonth }}: <b>{{ monthP.toFixed(1) }}%</b></p>
|
||||
<p>{{ i18n.ts.thisMonth }}<b>{{ monthP.toFixed(1) }}%</b></p>
|
||||
<div class="meter">
|
||||
<div class="val" :style="{ width: `${monthP}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p>{{ i18n.ts.thisYear }}: <b>{{ yearP.toFixed(1) }}%</b></p>
|
||||
<p>{{ i18n.ts.thisYear }}<b>{{ yearP.toFixed(1) }}%</b></p>
|
||||
<div class="meter">
|
||||
<div class="val" :style="{ width: `${yearP}%` }"></div>
|
||||
</div>
|
||||
@@ -168,13 +168,14 @@ defineExpose<WidgetComponentExpose>({
|
||||
}
|
||||
|
||||
> p {
|
||||
display: flex;
|
||||
margin: 0 0 2px 0;
|
||||
font-size: 0.75em;
|
||||
line-height: 18px;
|
||||
opacity: 0.8;
|
||||
|
||||
> b {
|
||||
margin-left: 2px;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,8 +51,8 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
||||
actions: userDetail.isFollowing ? [] : [
|
||||
{
|
||||
action: 'follow',
|
||||
title: t('_notification._actions.followBack')
|
||||
}
|
||||
title: t('_notification._actions.followBack'),
|
||||
},
|
||||
],
|
||||
}];
|
||||
}
|
||||
@@ -66,8 +66,8 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
||||
actions: [
|
||||
{
|
||||
action: 'reply',
|
||||
title: t('_notification._actions.reply')
|
||||
}
|
||||
title: t('_notification._actions.reply'),
|
||||
},
|
||||
],
|
||||
}];
|
||||
|
||||
@@ -80,8 +80,8 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
||||
actions: [
|
||||
{
|
||||
action: 'reply',
|
||||
title: t('_notification._actions.reply')
|
||||
}
|
||||
title: t('_notification._actions.reply'),
|
||||
},
|
||||
],
|
||||
}];
|
||||
|
||||
@@ -94,8 +94,8 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
||||
actions: [
|
||||
{
|
||||
action: 'showUser',
|
||||
title: getUserName(data.body.user)
|
||||
}
|
||||
title: getUserName(data.body.user),
|
||||
},
|
||||
],
|
||||
}];
|
||||
|
||||
@@ -108,14 +108,14 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
||||
actions: [
|
||||
{
|
||||
action: 'reply',
|
||||
title: t('_notification._actions.reply')
|
||||
title: t('_notification._actions.reply'),
|
||||
},
|
||||
...((data.body.note.visibility === 'public' || data.body.note.visibility === 'home') ? [
|
||||
{
|
||||
action: 'renote',
|
||||
title: t('_notification._actions.renote')
|
||||
}
|
||||
] : [])
|
||||
title: t('_notification._actions.renote'),
|
||||
},
|
||||
] : []),
|
||||
],
|
||||
}];
|
||||
|
||||
@@ -141,7 +141,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
||||
const dummy = `${encodeURIComponent(`${u.host}${u.pathname}`)}.png`;
|
||||
badge = `${origin}/proxy/${dummy}?${url.query({
|
||||
url: u.href,
|
||||
badge: '1'
|
||||
badge: '1',
|
||||
})}`;
|
||||
}
|
||||
}
|
||||
@@ -162,20 +162,12 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
||||
actions: [
|
||||
{
|
||||
action: 'showUser',
|
||||
title: getUserName(data.body.user)
|
||||
}
|
||||
title: getUserName(data.body.user),
|
||||
},
|
||||
],
|
||||
}];
|
||||
}
|
||||
|
||||
case 'pollVote':
|
||||
return [t('_notification.youGotPoll', { name: getUserName(data.body.user) }), {
|
||||
body: data.body.note.text || '',
|
||||
icon: data.body.user.avatarUrl,
|
||||
badge: iconUrl('poll-h'),
|
||||
data,
|
||||
}];
|
||||
|
||||
case 'pollEnded':
|
||||
return [t('_notification.pollEnded'), {
|
||||
body: data.body.note.text || '',
|
||||
@@ -192,12 +184,12 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
||||
actions: [
|
||||
{
|
||||
action: 'accept',
|
||||
title: t('accept')
|
||||
title: t('accept'),
|
||||
},
|
||||
{
|
||||
action: 'reject',
|
||||
title: t('reject')
|
||||
}
|
||||
title: t('reject'),
|
||||
},
|
||||
],
|
||||
}];
|
||||
|
||||
@@ -217,12 +209,12 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
||||
actions: [
|
||||
{
|
||||
action: 'accept',
|
||||
title: t('accept')
|
||||
title: t('accept'),
|
||||
},
|
||||
{
|
||||
action: 'reject',
|
||||
title: t('reject')
|
||||
}
|
||||
title: t('reject'),
|
||||
},
|
||||
],
|
||||
}];
|
||||
|
||||
@@ -230,7 +222,7 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(data
|
||||
return [data.body.header || data.body.body, {
|
||||
body: data.body.header && data.body.body,
|
||||
icon: data.body.icon,
|
||||
data
|
||||
data,
|
||||
}];
|
||||
|
||||
default:
|
||||
@@ -279,7 +271,7 @@ export async function createEmptyNotification() {
|
||||
silent: true,
|
||||
badge: iconUrl('null'),
|
||||
tag: 'read_notification',
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
res();
|
||||
@@ -288,7 +280,7 @@ export async function createEmptyNotification() {
|
||||
for (const n of
|
||||
[
|
||||
...(await self.registration.getNotifications({ tag: 'user_visible_auto_notification' })),
|
||||
...(await self.registration.getNotifications({ tag: 'read_notification' }))
|
||||
...(await self.registration.getNotifications({ tag: 'read_notification' })),
|
||||
]
|
||||
) {
|
||||
n.close();
|
||||
|
||||
Reference in New Issue
Block a user