Merge branch 'develop' of codeberg.org:calckey/calckey into develop
This commit is contained in:
commit
3066d6079a
22 changed files with 543 additions and 182 deletions
|
@ -835,7 +835,7 @@ muteThread: "Mute thread"
|
|||
unmuteThread: "Unmute thread"
|
||||
ffVisibility: "Follows/Followers Visibility"
|
||||
ffVisibilityDescription: "Allows you to configure who can see who you follow and who follows you."
|
||||
continueThread: "View thread continuation"
|
||||
continueThread: "Continue thread"
|
||||
deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
|
||||
incorrectPassword: "Incorrect password."
|
||||
voteConfirm: "Confirm your vote for \"{choice}\"?"
|
||||
|
|
|
@ -359,7 +359,7 @@ export function apiAccountMastodon(router: Router): void {
|
|||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = (await client.getBookmarks(ctx.query as any)) as any;
|
||||
const data = (await client.getBookmarks(limitToInt(ctx.query as any))) as any;
|
||||
let resp = data.data;
|
||||
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
|
||||
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
|
||||
|
@ -383,7 +383,7 @@ export function apiAccountMastodon(router: Router): void {
|
|||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getFavourites(ctx.query as any);
|
||||
const data = await client.getFavourites(limitToInt(ctx.query as any));
|
||||
let resp = data.data;
|
||||
for (let statIdx = 0; statIdx < resp.length; statIdx++) {
|
||||
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
|
||||
|
@ -407,7 +407,7 @@ export function apiAccountMastodon(router: Router): void {
|
|||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getMutes(ctx.query as any);
|
||||
const data = await client.getMutes(limitToInt(ctx.query as any));
|
||||
let resp = data.data;
|
||||
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
|
||||
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
|
||||
|
@ -425,7 +425,7 @@ export function apiAccountMastodon(router: Router): void {
|
|||
const accessTokens = ctx.headers.authorization;
|
||||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const data = await client.getBlocks(ctx.query as any);
|
||||
const data = await client.getBlocks(limitToInt(ctx.query as any));
|
||||
let resp = data.data;
|
||||
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
|
||||
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
|
||||
|
|
|
@ -4,6 +4,8 @@ import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
|
|||
import axios from "axios";
|
||||
import querystring from 'node:querystring'
|
||||
import qs from 'qs'
|
||||
import { limitToInt } from "./timeline.js";
|
||||
|
||||
function normalizeQuery(data: any) {
|
||||
const str = querystring.stringify(data);
|
||||
return qs.parse(str);
|
||||
|
@ -101,7 +103,7 @@ export function apiStatusMastodon(router: Router): void {
|
|||
const client = getClient(BASE_URL, accessTokens);
|
||||
try {
|
||||
const id = ctx.params.id;
|
||||
const data = await client.getStatusContext(id, ctx.query as any);
|
||||
const data = await client.getStatusContext(id, limitToInt(ctx.query as any));
|
||||
const status = await client.getStatus(id);
|
||||
const reactionsAxios = await axios.get(
|
||||
`${BASE_URL}/api/notes/reactions?noteId=${id}`,
|
||||
|
|
|
@ -15,13 +15,16 @@ export function limitToInt(q: ParsedUrlQuery) {
|
|||
}
|
||||
|
||||
export function argsToBools(q: ParsedUrlQuery) {
|
||||
// Values taken from https://docs.joinmastodon.org/client/intro/#boolean
|
||||
const toBoolean = (value: string) => !['0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].includes(value);
|
||||
|
||||
let object: any = q;
|
||||
if (q.only_media)
|
||||
if (typeof q.only_media === "string")
|
||||
object.only_media = q.only_media.toLowerCase() === "true";
|
||||
object.only_media = toBoolean(q.only_media);
|
||||
if (q.exclude_replies)
|
||||
if (typeof q.exclude_replies === "string")
|
||||
object.exclude_replies = q.exclude_replies.toLowerCase() === "true";
|
||||
object.exclude_replies = toBoolean(q.exclude_replies);
|
||||
return q;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<button class="nrvgflfu _button" @click.stop.prevent="toggle">
|
||||
<button class="nrvgflfu _button" @click.stop="toggle">
|
||||
<b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b>
|
||||
<span v-if="!modelValue">{{ label }}</span>
|
||||
</button>
|
||||
|
@ -36,6 +36,8 @@ const toggle = () => {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.nrvgflfu {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: inline-block;
|
||||
padding: 4px 8px;
|
||||
font-size: 0.8em;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="hoawjimk">
|
||||
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
|
||||
<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container" :class="{ dmWidth: inDm }">
|
||||
<div ref="gallery" :data-count="mediaList.filter(media => previewable(media)).length" @click.stop.prevent>
|
||||
<div ref="gallery" :data-count="mediaList.filter(media => previewable(media)).length" @click.stop>
|
||||
<template v-for="media in mediaList.filter(media => previewable(media))">
|
||||
<XVideo v-if="media.type.startsWith('video')" :key="media.id" :video="media"/>
|
||||
<XImage v-else-if="media.type.startsWith('image')" :key="media.id" class="image" :data-id="media.id" :image="media" :raw="raw"/>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" class="akbvjaqn" :class="{ isMe }" :to="url" :style="{ background: bgCss }">
|
||||
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" class="akbvjaqn" :class="{ isMe }" :to="url" :style="{ background: bgCss }" @click.stop>
|
||||
<img class="icon" :src="`/avatar/@${username}@${host}`" alt="">
|
||||
<span class="main">
|
||||
<span class="username">@{{ username }}</span>
|
||||
<span v-if="(host != localHost) || $store.state.showFullAcct" class="host">@{{ toUnicode(host) }}</span>
|
||||
</span>
|
||||
</MkA>
|
||||
<a v-else class="akbvjaqn" :href="url" target="_blank" rel="noopener" :style="{ background: bgCss }">
|
||||
<a v-else class="akbvjaqn" :href="url" target="_blank" rel="noopener" :style="{ background: bgCss }" @click.stop>
|
||||
<span class="main">
|
||||
<span class="username">@{{ username }}</span>
|
||||
<span class="host">@{{ toUnicode(host) }}</span>
|
||||
|
@ -42,8 +42,13 @@ const bgCss = bg.toRgbString();
|
|||
<style lang="scss" scoped>
|
||||
.akbvjaqn {
|
||||
display: inline-block;
|
||||
padding: 4px 8px 4px 4px;
|
||||
padding: 2px 8px 2px 2px;
|
||||
margin-block: 2px;
|
||||
border-radius: 999px;
|
||||
max-width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: clip;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--mention);
|
||||
|
||||
&.isMe {
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
</template>
|
||||
</I18n>
|
||||
<div class="info">
|
||||
<button ref="renoteTime" class="_button time" @click="showRenoteMenu()">
|
||||
<button ref="renoteTime" class="_button time" @click.stop="showRenoteMenu()">
|
||||
<i v-if="isMyRenote" class="ph-dots-three-outline ph-bold ph-lg dropdownIcon"></i>
|
||||
<MkTime :time="note.createdAt"/>
|
||||
</button>
|
||||
|
@ -33,24 +33,24 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<article class="article" @contextmenu.stop="onContextmenu" @click.self="router.push(notePage(appearNote))">
|
||||
<div class="main" @click.self="router.push(notePage(appearNote))">
|
||||
<article class="article" @contextmenu.stop="onContextmenu" @click="router.push(notePage(appearNote))">
|
||||
<div class="main">
|
||||
<div class="header-container">
|
||||
<MkAvatar class="avatar" :user="appearNote.user"/>
|
||||
<XNoteHeader class="header" :note="appearNote" :mini="true"/>
|
||||
</div>
|
||||
<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" :custom-emojis="appearNote.emojis"/>
|
||||
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis" @click.stop/>
|
||||
<XCwButton v-model="showContent" :note="appearNote"/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed, isLong }">
|
||||
<div class="text" @click.self="router.push(notePage(appearNote))">
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
<div class="text">
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis" @click.stop/>
|
||||
<!-- <a v-if="appearNote.renote != null" class="rp">RN:</a> -->
|
||||
<div v-if="translating || translation" class="translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
<div v-else class="translated">
|
||||
<div v-else class="translated" @click.stop>
|
||||
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
|
||||
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
</div>
|
||||
|
@ -61,36 +61,17 @@
|
|||
</div>
|
||||
<XPoll 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"><XNoteSimple :note="appearNote.renote"/></div>
|
||||
<button v-if="isLong && collapsed" class="fade _button" @click.stop.prevent="collapsed = false">
|
||||
<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote" @click.stop="router.push(notePage(appearNote.renote))"/></div>
|
||||
<button v-if="isLong && collapsed" class="fade _button" @click.stop="collapsed = false">
|
||||
<span>{{ i18n.ts.showMore }}</span>
|
||||
</button>
|
||||
<button v-else-if="isLong && !collapsed" class="showLess _button" @click.stop.prevent="collapsed = true">
|
||||
<button v-else-if="isLong && !collapsed" class="showLess _button" @click.stop="collapsed = true">
|
||||
<span>{{ i18n.ts.showLess }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
|
||||
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`" @click.stop><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
||||
<button v-tooltip.noDelay.bottom="i18n.ts.reply" class="button _button" @click="reply()">
|
||||
<template v-if="appearNote.reply"><i class="ph-arrow-u-up-left ph-bold ph-lg"></i></template>
|
||||
<template v-else><i class="ph-arrow-bend-up-left ph-bold ph-lg"></i></template>
|
||||
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
|
||||
</button>
|
||||
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
||||
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
|
||||
<button v-if="appearNote.myReaction == null" ref="reactButton" v-tooltip.noDelay.bottom="i18n.ts.reaction" class="button _button" @click="react()">
|
||||
<i class="ph-smiley ph-bold ph-lg"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
||||
<i class="ph-minus ph-bold ph-lg"></i>
|
||||
</button>
|
||||
<XQuoteButton class="button" :note="appearNote"/>
|
||||
<button ref="menuButton" v-tooltip.noDelay.bottom="i18n.ts.more" class="button _button" @click="menu()">
|
||||
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
|
||||
</button>
|
||||
</footer>
|
||||
<MkNoteFooter :note="appearNote"></MkNoteFooter>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
@ -113,15 +94,12 @@ import type * as misskey from 'calckey-js';
|
|||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||
import XNoteHeader from '@/components/MkNoteHeader.vue';
|
||||
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
|
||||
import XMediaList from '@/components/MkMediaList.vue';
|
||||
import XCwButton from '@/components/MkCwButton.vue';
|
||||
import MkNoteFooter from '@/components/MkNoteFooter.vue';
|
||||
import XPoll from '@/components/MkPoll.vue';
|
||||
import XStarButton from '@/components/MkStarButton.vue';
|
||||
import XRenoteButton from '@/components/MkRenoteButton.vue';
|
||||
import XQuoteButton from '@/components/MkQuoteButton.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';
|
||||
|
@ -170,7 +148,6 @@ const isRenote = (
|
|||
|
||||
const el = ref<HTMLElement>();
|
||||
const menuButton = ref<HTMLElement>();
|
||||
const starButton = ref<InstanceType<typeof XStarButton>>();
|
||||
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
||||
const renoteTime = ref<HTMLElement>();
|
||||
const reactButton = ref<HTMLElement>();
|
||||
|
@ -187,7 +164,6 @@ const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
|||
const translation = ref(null);
|
||||
const translating = ref(false);
|
||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5) : null;
|
||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||
|
||||
const keymap = {
|
||||
'r': () => reply(true),
|
||||
|
@ -229,14 +205,6 @@ function react(viaKeyboard = false): void {
|
|||
});
|
||||
}
|
||||
|
||||
function undoReact(note): void {
|
||||
const oldReaction = note.myReaction;
|
||||
if (!oldReaction) return;
|
||||
os.api('notes/reactions/delete', {
|
||||
noteId: note.id,
|
||||
});
|
||||
}
|
||||
|
||||
const currentClipPage = inject<Ref<misskey.entities.Clip> | null>('currentClipPage', null);
|
||||
|
||||
function onContextmenu(ev: MouseEvent): void {
|
||||
|
@ -342,19 +310,23 @@ function readPromo() {
|
|||
}
|
||||
}
|
||||
|
||||
&:hover > .article > .main > .footer > .button {
|
||||
opacity: 1;
|
||||
& > .article > .main {
|
||||
&:hover, &:focus-within {
|
||||
:deep(.footer .button) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
> .reply-to {
|
||||
& + .note-context {
|
||||
.line::before {
|
||||
content: "";
|
||||
display: block;
|
||||
margin-bottom: -10px;
|
||||
width: 2px;
|
||||
background-color: var(--divider);
|
||||
margin-inline: auto;
|
||||
margin-top: 16px;
|
||||
border-left: 2px solid var(--divider);
|
||||
margin-left: calc((var(--avatarSize) / 2) - 1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -477,7 +449,6 @@ function readPromo() {
|
|||
|
||||
> .body {
|
||||
margin-top: .7em;
|
||||
overflow: hidden;
|
||||
|
||||
> .cw {
|
||||
cursor: default;
|
||||
|
@ -585,6 +556,10 @@ function readPromo() {
|
|||
padding: 16px;
|
||||
border: solid 1px var(--renote);
|
||||
border-radius: 8px;
|
||||
transition: background .2s;
|
||||
&:hover, &:focus-within {
|
||||
background-color: var(--panelHighlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -594,36 +569,6 @@ function readPromo() {
|
|||
font-size: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
> .footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
> .button {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
opacity: 0.7;
|
||||
flex-grow: 1;
|
||||
max-width: 3.5em;
|
||||
width: max-content;
|
||||
min-width: max-content;
|
||||
&:first-of-type {
|
||||
margin-left: -.5em;
|
||||
}
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
|
||||
> .count {
|
||||
display: inline;
|
||||
margin: 0 0 0 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&.reacted {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@
|
|||
<XCwButton v-model="showContent" :note="appearNote"/>
|
||||
</p>
|
||||
<div v-show="appearNote.cw == null || showContent" class="content">
|
||||
<div class="text" @click.self="router.push(notePage(appearNote))">
|
||||
<div class="text">
|
||||
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
|
||||
<div v-if="translating || translation" class="translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
|
@ -68,7 +68,7 @@
|
|||
</div>
|
||||
<XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/>
|
||||
<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div>
|
||||
<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote" @click.stop="router.push(notePage(appearNote.renote))"/></div>
|
||||
</div>
|
||||
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
|
||||
</div>
|
||||
|
@ -291,17 +291,17 @@ function blur() {
|
|||
os.api('notes/children', {
|
||||
noteId: appearNote.id,
|
||||
limit: 30,
|
||||
depth: 6,
|
||||
depth: 12,
|
||||
}).then(res => {
|
||||
replies.value = res;
|
||||
directReplies.value = res.filter(note => note.replyId === appearNote.id || note.renoteId === appearNote.id);
|
||||
directReplies.value = res.filter(note => note.replyId === appearNote.id || note.renoteId === appearNote.id).reverse();
|
||||
});
|
||||
|
||||
if (appearNote.replyId) {
|
||||
os.api('notes/conversation', {
|
||||
noteId: appearNote.replyId,
|
||||
}).then(res => {
|
||||
conversation.value = res.reverse();
|
||||
conversation.value = res;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -335,7 +335,6 @@ onUnmounted(() => {
|
|||
.lxwezrsl {
|
||||
position: relative;
|
||||
transition: box-shadow 0.1s ease;
|
||||
overflow: hidden;
|
||||
contain: content;
|
||||
|
||||
&:focus-visible {
|
||||
|
@ -429,7 +428,12 @@ onUnmounted(() => {
|
|||
|
||||
> .article {
|
||||
padding: 32px;
|
||||
padding-bottom: 6px;
|
||||
&:last-child {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
font-size: 1.2em;
|
||||
overflow: clip;
|
||||
|
||||
> .header {
|
||||
display: flex;
|
||||
|
@ -530,6 +534,10 @@ onUnmounted(() => {
|
|||
padding: 16px;
|
||||
border: solid 1px var(--renote);
|
||||
border-radius: 8px;
|
||||
transition: background .2s;
|
||||
&:hover, &:focus-within {
|
||||
background-color: var(--panelHighlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -577,26 +585,72 @@ onUnmounted(() => {
|
|||
> .reply {
|
||||
border-top: solid 0.5px var(--divider);
|
||||
cursor: pointer;
|
||||
|
||||
padding-top: 24px;
|
||||
padding-bottom: 10px;
|
||||
@media (pointer: coarse) {
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
> .reply, .reply-to, .reply-to-more {
|
||||
transition: background-color 0.25s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--panelHighlight);
|
||||
// Hover
|
||||
.reply :deep(.main), .reply-to, .reply-to-more, :deep(.more) {
|
||||
position: relative;
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -12px -24px;
|
||||
bottom: -0px;
|
||||
background: var(--panelHighlight);
|
||||
border-radius: var(--radius);
|
||||
opacity: 0;
|
||||
transition: opacity .2s;
|
||||
z-index: -1;
|
||||
}
|
||||
&.reply-to, &.reply-to-more {
|
||||
&::before {
|
||||
inset: 0px 8px;
|
||||
}
|
||||
&:first-of-type::before {
|
||||
top: 12px;
|
||||
}
|
||||
}
|
||||
// &::after {
|
||||
// content: "";
|
||||
// position: absolute;
|
||||
// inset: -9999px;
|
||||
// background: var(--modalBg);
|
||||
// opacity: 0;
|
||||
// z-index: -2;
|
||||
// pointer-events: none;
|
||||
// transition: opacity .2s;
|
||||
// }
|
||||
&.more::before {
|
||||
inset: 0 !important;
|
||||
}
|
||||
&:hover, &:focus-within {
|
||||
&::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
// @media (pointer: coarse) {
|
||||
// &:has(.button:focus-within) {
|
||||
// z-index: 2;
|
||||
// --X13: transparent;
|
||||
// &::after {
|
||||
// opacity: 1;
|
||||
// backdrop-filter: var(--modalBgFilter);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
&.max-width_500px {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
|
||||
&.max-width_450px {
|
||||
|
||||
|
||||
> .reply-to-more:first-child {
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
|
164
packages/client/src/components/MkNoteFooter.vue
Normal file
164
packages/client/src/components/MkNoteFooter.vue
Normal file
|
@ -0,0 +1,164 @@
|
|||
<template>
|
||||
<footer ref="el" class="footer" @click.stop>
|
||||
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
|
||||
<button v-tooltip.noDelay.bottom="i18n.ts.reply" class="button _button" @click="reply()">
|
||||
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
|
||||
<template v-if="directReplies > 0">
|
||||
<p class="count">{{ directReplies }}</p>
|
||||
</template>
|
||||
<template v-else-if="appearNote.repliesCount > 0">
|
||||
<p class="count">{{ appearNote.repliesCount }}</p>
|
||||
</template>
|
||||
</button>
|
||||
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
|
||||
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
|
||||
<button v-if="appearNote.myReaction == null" ref="reactButton" v-tooltip.noDelay.bottom="i18n.ts.reaction" class="button _button" @click="react()">
|
||||
<i class="ph-smiley ph-bold ph-lg"></i>
|
||||
</button>
|
||||
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
|
||||
<i class="ph-minus ph-bold ph-lg"></i>
|
||||
</button>
|
||||
<XQuoteButton class="button" :note="appearNote"/>
|
||||
<button ref="menuButton" v-tooltip.noDelay.bottom="i18n.ts.more" class="button _button" @click="menu()">
|
||||
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
|
||||
</button>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { inject, ref } from 'vue';
|
||||
import type { Ref } from 'vue';
|
||||
import type * as misskey from 'calckey-js';
|
||||
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
|
||||
import XStarButton from '@/components/MkStarButton.vue';
|
||||
import XRenoteButton from '@/components/MkRenoteButton.vue';
|
||||
import XQuoteButton from '@/components/MkQuoteButton.vue';
|
||||
import { pleaseLogin } from '@/scripts/please-login';
|
||||
import * as os from '@/os';
|
||||
import { reactionPicker } from '@/scripts/reaction-picker';
|
||||
import { i18n } from '@/i18n';
|
||||
import { getNoteMenu } from '@/scripts/get-note-menu';
|
||||
import { deepClone } from '@/scripts/clone';
|
||||
import { useNoteCapture } from '@/scripts/use-note-capture';
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
directReplies;
|
||||
}>();
|
||||
|
||||
let note = $ref(deepClone(props.note));
|
||||
|
||||
const isRenote = (
|
||||
note.renote != null &&
|
||||
note.text == null &&
|
||||
note.fileIds.length === 0 &&
|
||||
note.poll == null
|
||||
);
|
||||
|
||||
const el = ref<HTMLElement>();
|
||||
const menuButton = ref<HTMLElement>();
|
||||
const starButton = ref<InstanceType<typeof XStarButton>>();
|
||||
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
|
||||
const reactButton = ref<HTMLElement>();
|
||||
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
|
||||
const isDeleted = ref(false);
|
||||
const translation = ref(null);
|
||||
const translating = ref(false);
|
||||
|
||||
useNoteCapture({
|
||||
rootEl: el,
|
||||
note: $$(appearNote),
|
||||
isDeletedRef: isDeleted,
|
||||
});
|
||||
|
||||
|
||||
function reply(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
os.post({
|
||||
reply: appearNote,
|
||||
animation: !viaKeyboard,
|
||||
}, () => {
|
||||
focus();
|
||||
});
|
||||
}
|
||||
|
||||
function react(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
blur();
|
||||
reactionPicker.show(reactButton.value, reaction => {
|
||||
os.api('notes/reactions/create', {
|
||||
noteId: appearNote.id,
|
||||
reaction: reaction,
|
||||
});
|
||||
}, () => {
|
||||
focus();
|
||||
});
|
||||
}
|
||||
|
||||
function undoReact(note): void {
|
||||
const oldReaction = note.myReaction;
|
||||
if (!oldReaction) return;
|
||||
os.api('notes/reactions/delete', {
|
||||
noteId: note.id,
|
||||
});
|
||||
}
|
||||
|
||||
const currentClipPage = inject<Ref<misskey.entities.Clip> | null>('currentClipPage', null);
|
||||
|
||||
|
||||
function menu(viaKeyboard = false): void {
|
||||
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClipPage }), menuButton.value, {
|
||||
viaKeyboard,
|
||||
}).then(focus);
|
||||
}
|
||||
|
||||
function focus() {
|
||||
el.value.focus();
|
||||
}
|
||||
|
||||
function blur() {
|
||||
el.value.blur();
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.footer {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
pointer-events: none; // Allow clicking anything w/out pointer-events: all; to open post
|
||||
|
||||
> .button {
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
opacity: 0.7;
|
||||
flex-grow: 1;
|
||||
max-width: 3.5em;
|
||||
width: max-content;
|
||||
min-width: max-content;
|
||||
pointer-events: all;
|
||||
transition: opacity .2s;
|
||||
&:first-of-type {
|
||||
margin-left: -.5em;
|
||||
}
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
}
|
||||
|
||||
> .count {
|
||||
display: inline;
|
||||
margin: 0 0 0 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&.reacted {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -2,7 +2,7 @@
|
|||
<header class="kkwtjztg">
|
||||
<div class="user-info">
|
||||
<div>
|
||||
<MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)">
|
||||
<MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)" @click.stop>
|
||||
<MkUserName :user="note.user" class="mkusername">
|
||||
<span v-if="note.user.isBot" class="is-bot">bot</span>
|
||||
</MkUserName>
|
||||
|
@ -47,6 +47,8 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.kkwtjztg {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
<template>
|
||||
<div v-size="{ max: [450] }" class="wrpstxzv" :class="{ children: depth > 1 }">
|
||||
<div v-size="{ max: [450, 500] }" class="wrpstxzv" :class="{ children: depth > 1, singleStart: replies.length == 1, firstColumn: depth == 1 && conversation }">
|
||||
<div v-if="conversation && depth > 1" class="line"></div>
|
||||
<div class="main" @click="router.push(notePage(note))">
|
||||
<div class="avatar-container">
|
||||
<MkAvatar class="avatar" :user="note.user"/>
|
||||
<div class="line"></div>
|
||||
<div v-if="(!conversation) || replies.length > 0" class="line"></div>
|
||||
</div>
|
||||
<div class="body">
|
||||
<XNoteHeader class="header" :note="note" :mini="true"/>
|
||||
|
@ -13,16 +14,21 @@
|
|||
<XCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent" class="content" @click="router.push(notePage(note))">
|
||||
<MkSubNoteContent class="text" :note="note"/>
|
||||
<MkSubNoteContent class="text" :note="note" :detailed="true"/>
|
||||
</div>
|
||||
</div>
|
||||
<MkNoteFooter :note="note" :directReplies="replies.length"></MkNoteFooter>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="conversation">
|
||||
<template v-if="depth < 5">
|
||||
<template v-if="replies.length == 1">
|
||||
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply single" :conversation="conversation" :depth="depth"/>
|
||||
</template>
|
||||
<template v-else-if="depth < 5">
|
||||
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :conversation="conversation" :depth="depth + 1"/>
|
||||
</template>
|
||||
<div v-else-if="replies.length > 0" class="more">
|
||||
<div class="line"></div>
|
||||
<MkA class="text _link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ph-caret-double-right ph-bold ph-lg"></i></MkA>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -35,6 +41,7 @@ import * as misskey from 'calckey-js';
|
|||
import XNoteHeader from '@/components/MkNoteHeader.vue';
|
||||
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
|
||||
import XCwButton from '@/components/MkCwButton.vue';
|
||||
import MkNoteFooter from '@/components/MkNoteFooter.vue';
|
||||
import { notePage } from '@/filters/note';
|
||||
import { useRouter } from '@/router';
|
||||
import * as os from '@/os';
|
||||
|
@ -53,16 +60,15 @@ const props = withDefaults(defineProps<{
|
|||
});
|
||||
|
||||
let showContent = $ref(false);
|
||||
const replies: misskey.entities.Note[] = props.conversation?.filter(item => item.replyId === props.note.id || item.renoteId === props.note.id) ?? [];
|
||||
const replies: misskey.entities.Note[] = props.conversation?.filter(item => item.replyId === props.note.id || item.renoteId === props.note.id).reverse() ?? [];
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrpstxzv {
|
||||
padding: 16px 32px;
|
||||
|
||||
|
||||
&.children {
|
||||
padding: 10px 0 0 16px;
|
||||
padding: 10px 0 0 var(--indent);
|
||||
padding-left: var(--indent) !important;
|
||||
font-size: 1em;
|
||||
cursor: auto;
|
||||
|
||||
|
@ -71,6 +77,7 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
> .main {
|
||||
display: flex;
|
||||
|
||||
|
@ -89,6 +96,9 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
|
|||
flex: 1;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
margin: 0 -200px;
|
||||
padding: 0 200px;
|
||||
overflow: clip;
|
||||
@media (pointer: coarse) {
|
||||
cursor: default;
|
||||
}
|
||||
|
@ -120,14 +130,66 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child > .main > .body {
|
||||
margin-top: -200px;
|
||||
padding-top: 200px;
|
||||
}
|
||||
&.reply {
|
||||
--avatarSize: 38px;
|
||||
.avatar-container {
|
||||
margin-right: 8px !important;
|
||||
}
|
||||
:deep(.footer) {
|
||||
font-size: .9em;
|
||||
}
|
||||
}
|
||||
> .reply, > .more {
|
||||
border-left: solid 0.5px var(--divider);
|
||||
margin-top: 10px;
|
||||
&.single {
|
||||
padding: 0 !important;
|
||||
> .line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .more {
|
||||
padding: 10px 0 0 16px;
|
||||
display: flex;
|
||||
padding-block: 10px;
|
||||
font-weight: 600;
|
||||
> .line {
|
||||
flex-grow: 0 !important;
|
||||
margin-top: -10px !important;
|
||||
margin-bottom: 10px !important;
|
||||
margin-right: 10px !important;
|
||||
&::before {
|
||||
border-left-style: dashed !important;
|
||||
border-bottom-left-radius: 100px !important;
|
||||
}
|
||||
}
|
||||
i {
|
||||
font-size: 1em !important;
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
a {
|
||||
position: static;
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
&::after {
|
||||
content: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.reply, &.reply-to, &.reply-to-more {
|
||||
> .main:hover, > .main:focus-within {
|
||||
:deep(.footer .button) {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.reply-to, &.reply-to-more {
|
||||
|
@ -135,41 +197,110 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
|
|||
&:first-child {
|
||||
padding-top: 30px;
|
||||
}
|
||||
.avatar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-right: 14px;
|
||||
width: var(--avatarSize);
|
||||
> .avatar {
|
||||
width: var(--avatarSize);
|
||||
height: var(--avatarSize);
|
||||
margin: 0;
|
||||
}
|
||||
> .line {
|
||||
width: var(--avatarSize);
|
||||
.line::before {
|
||||
margin-bottom: -16px;
|
||||
}
|
||||
}
|
||||
|
||||
// Reply Lines
|
||||
&.reply, &.reply-to, &.reply-to-more {
|
||||
--indent: calc(var(--avatarSize) - 5px);
|
||||
> .main {
|
||||
> .avatar-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 2px;
|
||||
background-color: var(--divider);
|
||||
margin-inline: auto;
|
||||
.note > & {
|
||||
margin-bottom: -16px;
|
||||
}
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-right: 14px;
|
||||
width: var(--avatarSize);
|
||||
> .avatar {
|
||||
width: var(--avatarSize);
|
||||
height: var(--avatarSize);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .main > .body {
|
||||
padding-bottom: 16px;
|
||||
.line {
|
||||
position: relative;
|
||||
width: var(--avatarSize);
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
margin-bottom: -10px;
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-left: 2px solid var(--X13);
|
||||
margin-left: calc((var(--avatarSize) / 2) - 1px);
|
||||
width: calc(var(--indent) / 2);
|
||||
inset-block: 0;
|
||||
min-height: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.reply-to, &.reply-to-more {
|
||||
> .main > .avatar-container > .line {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
}
|
||||
&.single, &.singleStart {
|
||||
> .main > .avatar-container > .line {
|
||||
margin-bottom: -10px !important;
|
||||
}
|
||||
}
|
||||
.reply.children:not(:last-child) { // Line that goes through multiple replies
|
||||
position: relative;
|
||||
> .line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
// Reply line connectors
|
||||
.reply.children:not(.single) {
|
||||
position: relative;
|
||||
> .line {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-left: 2px solid var(--X13);
|
||||
border-bottom: 2px solid var(--X13);
|
||||
margin-left: calc((var(--avatarSize) / 2) - 1px);
|
||||
width: calc(var(--indent) / 2);
|
||||
height: calc((var(--avatarSize) / 2));
|
||||
border-bottom-left-radius: calc(var(--indent) / 2);
|
||||
top: 8px;
|
||||
}
|
||||
}
|
||||
&:not(:last-child) > .line::after {
|
||||
mask: linear-gradient(to right, transparent 2px, black 2px);
|
||||
-webkit-mask: linear-gradient(to right, transparent 2px, black 2px);
|
||||
}
|
||||
}
|
||||
|
||||
&.max-width_500px {
|
||||
:not(.reply) > & {
|
||||
.reply {
|
||||
--avatarSize: 24px;
|
||||
--indent: calc(var(--avatarSize) - 4px);
|
||||
}
|
||||
}
|
||||
&.firstColumn {
|
||||
> .main, > .line, > .children:not(.single) > .line {
|
||||
--avatarSize: 35px;
|
||||
--indent: 35px;
|
||||
}
|
||||
> .children:not(.single) {
|
||||
padding-left: 28px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.max-width_450px {
|
||||
padding: 14px 16px;
|
||||
&.reply-to, &.reply-to-more {
|
||||
padding: 14px 16px;
|
||||
padding-top: 14px !important;
|
||||
padding-bottom: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="tivcixzd" :class="{ done: closed || isVoted }">
|
||||
<ul>
|
||||
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="{ voted: choice.voted }" @click="vote(i)">
|
||||
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="{ voted: choice.voted }" @click.stop="vote(i)">
|
||||
<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
|
||||
<span>
|
||||
<template v-if="choice.isVoted"><i class="ph-check ph-bold ph-lg"></i></template>
|
||||
|
@ -13,7 +13,7 @@
|
|||
<p v-if="!readOnly">
|
||||
<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
|
||||
<span> · </span>
|
||||
<a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
|
||||
<a v-if="!closed && !isVoted" @click.stop="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
|
||||
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
|
||||
<span v-else-if="closed">{{ i18n.ts._poll.closed }}</span>
|
||||
<span v-if="remaining > 0"> · {{ timer }}</span>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
<span v-if="visibility === 'specified'"><i class="ph-envelope-simple-open ph-bold ph-lg"></i></span>
|
||||
</button>
|
||||
<button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="ph-file-code ph-bold ph-lg"></i></button>
|
||||
<button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'ph-arrow-bend-up-left ph-bold ph-lg' : renote ? 'ph-quotes ph-bold ph-lg' : 'ph-paper-plane-tilt ph-bold ph-lg'"></i></button>
|
||||
<button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'ph-arrow-u-up-left ph-bold ph-lg' : renote ? 'ph-quotes ph-bold ph-lg' : 'ph-paper-plane-tilt ph-bold ph-lg'"></i></button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="form" :class="{ fixed }">
|
||||
|
@ -796,6 +796,8 @@ onMounted(() => {
|
|||
}
|
||||
|
||||
> .submit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin: 16px 16px 16px 0;
|
||||
padding: 0 12px;
|
||||
line-height: 34px;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
ref="buttonRef"
|
||||
v-ripple="canToggle"
|
||||
class="hkzvhatu _button"
|
||||
:class="{ reacted: note.myReaction == reaction, canToggle }"
|
||||
:class="{ reacted: note.myReaction == reaction, canToggle, newlyAdded: !isInitial }"
|
||||
@click="toggleReaction()"
|
||||
>
|
||||
<XReactionIcon class="icon" :reaction="reaction" :custom-emojis="note.emojis"/>
|
||||
|
@ -13,7 +13,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import * as misskey from 'calckey-js';
|
||||
import XDetails from '@/components/MkReactionsViewer.details.vue';
|
||||
import XReactionIcon from '@/components/MkReactionIcon.vue';
|
||||
|
@ -55,20 +55,6 @@ const toggleReaction = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const anime = () => {
|
||||
if (document.hidden) return;
|
||||
|
||||
// TODO: 新しくリアクションが付いたことが視覚的に分かりやすいアニメーション
|
||||
};
|
||||
|
||||
watch(() => props.count, (newCount, oldCount) => {
|
||||
if (oldCount < newCount) anime();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.isInitial) anime();
|
||||
});
|
||||
|
||||
useTooltip(buttonRef, async (showing) => {
|
||||
const reactions = await os.apiGet('notes/reactions', {
|
||||
noteId: props.note.id,
|
||||
|
@ -97,7 +83,25 @@ useTooltip(buttonRef, async (showing) => {
|
|||
margin: 2px;
|
||||
padding: 0 6px;
|
||||
border-radius: 4px;
|
||||
|
||||
pointer-events: all;
|
||||
&.newlyAdded {
|
||||
animation: scaleInSmall .3s cubic-bezier(0,0,0,1.2);
|
||||
:deep(.mk-emoji) {
|
||||
animation: scaleIn .4s cubic-bezier(0.7, 0, 0, 1.5);
|
||||
}
|
||||
}
|
||||
:deep(.mk-emoji) {
|
||||
transition: transform .4s cubic-bezier(0,0,0,6);
|
||||
}
|
||||
&.reacted :deep(.mk-emoji) {
|
||||
transition: transform .4s cubic-bezier(0,0,0,1);
|
||||
}
|
||||
&:active {
|
||||
:deep(.mk-emoji) {
|
||||
transition: transform .4s cubic-bezier(0,0,0,1);
|
||||
transform: scale(.85);
|
||||
}
|
||||
}
|
||||
&.canToggle {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
|
||||
|
@ -119,6 +123,7 @@ useTooltip(buttonRef, async (showing) => {
|
|||
|
||||
> .count {
|
||||
color: var(--fgOnAccent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
|
|
|
@ -21,9 +21,10 @@ const isMe = computed(() => $i && $i.id === props.note.userId);
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.tdflqwzn {
|
||||
margin: 4px -2px 0 -2px;
|
||||
margin-inline: -2px;
|
||||
margin-top: .2em;
|
||||
width: 100%;
|
||||
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -6,18 +6,23 @@
|
|||
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
|
||||
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">{{ i18n.ts.quoteAttached }}: ...</MkA>
|
||||
</div>
|
||||
<template v-if="detailed">
|
||||
<!-- <div v-if="note.renoteId" class="renote">
|
||||
<XNoteSimple :note="note.renote"/>
|
||||
</div> -->
|
||||
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
|
||||
</template>
|
||||
<div v-if="note.files.length > 0">
|
||||
<summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary>
|
||||
<XMediaList :media-list="note.files"/>
|
||||
</div>
|
||||
<div v-if="note.poll">
|
||||
<summary>{{ i18n.ts.poll }}</summary>
|
||||
<XPoll :note="note"/>
|
||||
</div>
|
||||
<button v-if="isLong && collapsed" class="fade _button" @click.stop.prevent="collapsed = false">
|
||||
<button v-if="isLong && collapsed" class="fade _button" @click.stop="collapsed = false">
|
||||
<span>{{ i18n.ts.showMore }}</span>
|
||||
</button>
|
||||
<button v-if="isLong && !collapsed" class="showLess _button" @click.stop.prevent="collapsed = true">
|
||||
<button v-if="isLong && !collapsed" class="showLess _button" @click.stop="collapsed = true">
|
||||
<span>{{ i18n.ts.showLess }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -26,15 +31,19 @@
|
|||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import * as misskey from 'calckey-js';
|
||||
import * as mfm from 'mfm-js';
|
||||
import XNoteSimple from '@/components/MkNoteSimple.vue';
|
||||
import XMediaList from '@/components/MkMediaList.vue';
|
||||
import XPoll from '@/components/MkPoll.vue';
|
||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
|
||||
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
detailed?: boolean;
|
||||
}>();
|
||||
|
||||
|
||||
const isLong = (
|
||||
props.note.cw == null && props.note.text != null && (
|
||||
(props.note.text.split('\n').length > 9) ||
|
||||
|
@ -42,12 +51,14 @@ const isLong = (
|
|||
)
|
||||
);
|
||||
const collapsed = $ref(props.note.cw == null && isLong);
|
||||
const urls = props.note.text ? extractUrlFromMfm(mfm.parse(props.note.text)) : null;
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wrmlmaau {
|
||||
overflow-wrap: break-word;
|
||||
|
||||
|
||||
> .body {
|
||||
> .reply {
|
||||
margin-right: 6px;
|
||||
|
@ -61,6 +72,10 @@ const collapsed = $ref(props.note.cw == null && isLong);
|
|||
}
|
||||
}
|
||||
|
||||
> .mk-url-preview {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&.collapsed {
|
||||
position: relative;
|
||||
max-height: 9em;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
|
||||
<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`" @click.stop>
|
||||
<button class="disablePlayer" :title="i18n.ts.disablePlayer" @click="playerEnabled = false"><i class="ph-x ph-bold ph-lg"></i></button>
|
||||
<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
|
||||
</div>
|
||||
<div v-else-if="tweetId && tweetExpanded" ref="twitter" class="twitter">
|
||||
<div v-else-if="tweetId && tweetExpanded" ref="twitter" class="twitter" @click.stop>
|
||||
<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&hideCard=false&hideThread=false&lang=en&theme=${$store.state.darkMode ? 'dark' : 'light'}&id=${tweetId}`"></iframe>
|
||||
</div>
|
||||
<div v-else v-size="{ max: [400, 350] }" class="mk-url-preview">
|
||||
<div v-else v-size="{ max: [400, 350] }" class="mk-url-preview" @click.stop>
|
||||
<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
|
||||
<component :is="self ? 'MkA' : 'a'" v-if="!fetching" class="link" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
|
||||
<div v-if="thumbnail" class="thumbnail" :style="`background-image: url('${thumbnail}')`">
|
||||
|
@ -214,9 +214,10 @@ onUnmounted(() => {
|
|||
border: 1px solid var(--divider);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
transition: background .2s;
|
||||
&:hover, &:focus-within {
|
||||
text-decoration: none;
|
||||
background-color: var(--panelHighlight);
|
||||
> article > header > h1 {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<img class="inner" :src="url" decoding="async"/>
|
||||
<MkUserOnlineIndicator v-if="showIndicator && user.instance == null" class="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">
|
||||
<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" @click.stop>
|
||||
<img class="inner" :src="url" decoding="async"/>
|
||||
<MkUserOnlineIndicator v-if="showIndicator && user.instance == null" class="indicator" :user="user"/>
|
||||
</MkA>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<component
|
||||
:is="self ? 'MkA' : 'a'" ref="el" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
|
||||
@contextmenu.stop="() => {}"
|
||||
@contextmenu.stop="() => {}" @click.stop
|
||||
>
|
||||
<template v-if="!self">
|
||||
<span class="schema">{{ schema }}//</span>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<MkStickyContainer>
|
||||
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="800">
|
||||
<div class="fcuexfpr">
|
||||
<div class="fcuexfpr" v-size="{ max: [500, 350] }">
|
||||
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
|
||||
<div v-if="note" class="note">
|
||||
<div v-if="showNext" class="_gap">
|
||||
|
@ -202,5 +202,13 @@ definePageMetadata(computed(() => note ? {
|
|||
}
|
||||
}
|
||||
}
|
||||
#calckey_app > :not(.mk-deck) {
|
||||
&.max-width_500px > .note {
|
||||
margin-inline: -24px;
|
||||
}
|
||||
&.max-width_350px > .note {
|
||||
margin-inline: -12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -32,7 +32,7 @@ html {
|
|||
overflow-wrap: break-word;
|
||||
font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.35;
|
||||
line-height: 1.6;
|
||||
text-size-adjust: 100%;
|
||||
tab-size: 2;
|
||||
|
||||
|
@ -155,6 +155,10 @@ hr {
|
|||
box-shadow: 0px 4px 32px var(--shadow) !important;
|
||||
}
|
||||
|
||||
.swiper {
|
||||
overflow: clip !important;
|
||||
}
|
||||
|
||||
._button {
|
||||
appearance: none;
|
||||
display: inline-block;
|
||||
|
@ -479,6 +483,7 @@ hr {
|
|||
}
|
||||
|
||||
._link {
|
||||
position: relative;
|
||||
color: var(--link);
|
||||
|
||||
&:after {
|
||||
|
@ -680,3 +685,19 @@ hr {
|
|||
width: 1.25em;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
|
||||
@media(prefers-reduced-motion: no-preference) {
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes scaleInSmall {
|
||||
from {
|
||||
transform: scale(.8);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue