Compare commits

...

23 Commits

Author SHA1 Message Date
syuilo
af6a578fa6 13.0.0-rc.2 2023-01-14 13:49:13 +09:00
tamaina
73d735a1f7 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2023-01-14 04:48:17 +00:00
tamaina
b8b1899a9f chore: fix ref name (pages/timeline.vue) 2023-01-14 04:47:59 +00:00
syuilo
d52f0617a1 fix(server): ドライブ容量超過時のエラーが適切にレスポンスされない問題を修正
Fix #9550
2023-01-14 13:41:53 +09:00
syuilo
c730973294 多分 fix #9551 2023-01-14 13:36:18 +09:00
syuilo
2c2e064871 refactor(client): use css modules 2023-01-14 13:29:41 +09:00
syuilo
e3c39d4b52 refactor(client): use css modules 2023-01-14 12:45:20 +09:00
syuilo
5da74897ae refactor(client): use css modules 2023-01-14 12:43:54 +09:00
syuilo
4b1009b34e refactor(client): use css modules 2023-01-14 12:30:32 +09:00
syuilo
203a7ad073 refactor(client): use css modules 2023-01-14 12:15:02 +09:00
syuilo
34a7b52105 13.0.0-rc.1 2023-01-14 12:00:07 +09:00
syuilo
30fc166c08 refactor(client): use css modules 2023-01-14 11:59:08 +09:00
syuilo
c84d86b368 refactor(client): use css modules 2023-01-14 11:48:30 +09:00
syuilo
1e5d4db0a1 refactor(client): use css modules 2023-01-14 11:46:22 +09:00
syuilo
5e02f0d325 refactor(client): use css modules 2023-01-14 11:39:35 +09:00
syuilo
ce5506f331 refactor(client): use css modules 2023-01-14 11:23:02 +09:00
syuilo
91105845d8 refactor(client): use css modules 2023-01-14 11:18:12 +09:00
syuilo
2bedc084a3 tweak MkRolePreview 2023-01-14 11:14:14 +09:00
syuilo
027ef1ea4a Update vite.config.ts 2023-01-14 11:10:41 +09:00
syuilo
668aa17eef refactor(client): use css modules 2023-01-14 10:57:34 +09:00
syuilo
ebf8ef22e4 🎨 2023-01-14 10:49:53 +09:00
syuilo
bcb5182e86 Webhookの作成可能数を設定可能に 2023-01-14 10:48:11 +09:00
syuilo
f45059b7b1 fix 2023-01-14 10:46:40 +09:00
34 changed files with 986 additions and 886 deletions

View File

@@ -76,6 +76,7 @@ You should also include the user name that made the change.
- 非モデレーターでも、権限を持つロールをアサインされたユーザーはインスタンスの招待コードを発行できるように @syuilo
- 非モデレーターでも、権限を持つロールをアサインされたユーザーはカスタム絵文字の追加、編集、削除を行えるように @syuilo
- ハードワードミュートの最大文字数を設定可能に @syuilo
- Webhookの作成可能数を設定可能に @syuilo
- Server: signToActivityPubGet is set to true by default @syuilo
- Server: improve syslog performance @syuilo
- Server: Use undici instead of node-fetch and got @tamaina
@@ -132,6 +133,7 @@ You should also include the user name that made the change.
- Server: 非公開のクリップのURLでOGPレンダリングされる問題を修正 @syuilo
- Server: アンテナタイムライン(ストリーミング)が、フォローしていないユーザーの鍵投稿も拾ってしまう @syuilo
- Server: follow request list api pagination @sim1222
- Server: ドライブ容量超過時のエラーが適切にレスポンスされない問題を修正 @syuilo
- Client: パスワードマネージャーなどでユーザー名がオートコンプリートされない問題を修正 @massongit
- Client: 日付形式の文字列などがカスタム絵文字として表示されるのを修正 @syuilo
- Client: case insensitive emoji search @saschanaz

View File

@@ -963,6 +963,7 @@ _role:
driveCapacity: "ドライブ容量"
antennaMax: "アンテナの作成可能数"
wordMuteMax: "ワードミュートの最大文字数"
webhookMax: "Webhookの作成可能数"
_condition:
isLocal: "ローカルユーザー"
isRemote: "リモートユーザー"

View File

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

View File

@@ -22,6 +22,7 @@ export type RoleOptions = {
driveCapacityMb: number;
antennaLimit: number;
wordMuteLimit: number;
webhookLimit: number;
};
export const DEFAULT_ROLE: RoleOptions = {
@@ -33,6 +34,7 @@ export const DEFAULT_ROLE: RoleOptions = {
driveCapacityMb: 100,
antennaLimit: 5,
wordMuteLimit: 200,
webhookLimit: 3,
};
@Injectable()
@@ -203,6 +205,7 @@ export class RoleService implements OnApplicationShutdown {
driveCapacityMb: Math.max(...getOptionValues('driveCapacityMb')),
antennaLimit: Math.max(...getOptionValues('antennaLimit')),
wordMuteLimit: Math.max(...getOptionValues('wordMuteLimit')),
webhookLimit: Math.max(...getOptionValues('webhookLimit')),
};
}

View File

@@ -62,6 +62,7 @@ export class RoleEntityService {
isModerator: role.isModerator,
canEditMembersByModerator: role.canEditMembersByModerator,
options: roleOptions,
usersCount: assigns.length,
...(opts.detail ? {
users: this.userEntityService.packMany(assigns.map(x => x.userId), me),
} : {}),

View File

@@ -90,7 +90,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
}
const meta = await this.metaService.fetch();
const instance = await this.metaService.fetch();
try {
// Create file
@@ -102,8 +102,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
folderId: ps.folderId,
force: ps.force,
sensitive: ps.isSensitive,
requestIp: meta.enableIpLogging ? ip : null,
requestHeaders: meta.enableIpLogging ? headers : null,
requestIp: instance.enableIpLogging ? ip : null,
requestHeaders: instance.enableIpLogging ? headers : null,
});
return await this.driveFileEntityService.pack(driveFile, { self: true });
} catch (err) {
@@ -116,7 +116,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
}
throw new ApiError();
} finally {
cleanup!();
cleanup!();
}
});
}

View File

@@ -60,7 +60,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId);
const query = this.queryService.makePaginationQuery(this.followRequestsRepository.createQueryBuilder('request'), ps.sinceId, ps.untilId)
.andWhere('request.followeeId = :meId', { meId: me.id });
const requests = await query
.take(ps.limit)

View File

@@ -173,7 +173,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
if (ps.mutedWords !== undefined) {
// TODO: ちゃんと数える
const length = JSON.stringify(ps.mutedWords).length;
if (length > (await this.roleService.getUserRoleOptions(user.id)).antennaLimit) {
if (length > (await this.roleService.getUserRoleOptions(user.id)).wordMuteLimit) {
throw new ApiError(meta.errors.tooManyMutedWords);
}

View File

@@ -5,6 +5,7 @@ import type { WebhooksRepository } from '@/models/index.js';
import { webhookEventTypes } from '@/models/entities/Webhook.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['webhooks'],
@@ -12,6 +13,14 @@ export const meta = {
requireCredential: true,
kind: 'write:account',
errors: {
tooManyWebhooks: {
message: 'You cannot create webhook any more.',
code: 'TOO_MANY_WEBHOOKS',
id: '87a9bb19-111e-4e37-81d3-a3e7426453b0',
},
},
} as const;
export const paramDef = {
@@ -38,8 +47,16 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private idService: IdService,
private globalEventService: GlobalEventService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
const currentWebhooksCount = await this.webhooksRepository.countBy({
userId: me.id,
});
if (currentWebhooksCount > (await this.roleService.getUserRoleOptions(me.id)).webhookLimit) {
throw new ApiError(meta.errors.tooManyWebhooks);
}
const webhook = await this.webhooksRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),

View File

@@ -1,5 +1,5 @@
<template>
<svg class="mbcofsoe" viewBox="0 0 10 10" preserveAspectRatio="none">
<svg :class="$style.root" viewBox="0 0 10 10" preserveAspectRatio="none">
<template v-if="props.graduations === 'dots'">
<circle
v-for="(angle, i) in graduationsMajor"
@@ -39,8 +39,7 @@
-->
<line
class="s"
:class="{ animate: !disableSAnimate && sAnimation !== 'none', elastic: sAnimation === 'elastic', easeOut: sAnimation === 'easeOut' }"
:class="[$style.s, { [$style.animate]: !disableSAnimate && sAnimation !== 'none', [$style.elastic]: sAnimation === 'elastic', [$style.easeOut]: sAnimation === 'easeOut' }]"
:x1="5 - (0 * (sHandLengthRatio * handsTailLength))"
:y1="5 + (1 * (sHandLengthRatio * handsTailLength))"
:x2="5 + (0 * ((sHandLengthRatio * 5) - handsPadding))"
@@ -205,21 +204,21 @@ onBeforeUnmount(() => {
});
</script>
<style lang="scss" scoped>
.mbcofsoe {
<style lang="scss" module>
.root {
display: block;
}
> .s {
will-change: transform;
transform-origin: 50% 50%;
.s {
will-change: transform;
transform-origin: 50% 50%;
&.animate.elastic {
transition: transform .2s cubic-bezier(.4,2.08,.55,.44);
}
&.animate.elastic {
transition: transform .2s cubic-bezier(.4,2.08,.55,.44);
}
&.animate.easeOut {
transition: transform .7s cubic-bezier(0,.7,.3,1);
}
&.animate.easeOut {
transition: transform .7s cubic-bezier(0,.7,.3,1);
}
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<button class="nrvgflfu _button" @click="toggle">
<button class="_button" :class="$style.root" @click="toggle">
<b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b>
<span v-if="!modelValue">{{ label }}</span>
<span v-if="!modelValue" :class="$style.label">{{ label }}</span>
</button>
</template>
@@ -34,8 +34,8 @@ const toggle = () => {
};
</script>
<style lang="scss" scoped>
.nrvgflfu {
<style lang="scss" module>
.root {
display: inline-block;
padding: 4px 8px;
font-size: 0.7em;
@@ -46,17 +46,17 @@ const toggle = () => {
&:hover {
background: var(--cwHoverBg);
}
}
> span {
margin-left: 4px;
.label {
margin-left: 4px;
&:before {
content: '(';
}
&:before {
content: '(';
}
&:after {
content: ')';
}
&:after {
content: ')';
}
}
</style>

View File

@@ -1,11 +1,11 @@
<template>
<span class="zjobosdg">
<span>
<span v-text="hh"></span>
<span class="colon" :class="{ showColon }">:</span>
<span :class="[$style.colon, { [$style.showColon]: showColon }]">:</span>
<span v-text="mm"></span>
<span v-if="showS" class="colon" :class="{ showColon }">:</span>
<span v-if="showS" :class="[$style.colon, { [$style.showColon]: showColon }]">:</span>
<span v-if="showS" v-text="ss"></span>
<span v-if="showMs" class="colon" :class="{ showColon }">:</span>
<span v-if="showMs" :class="[$style.colon, { [$style.showColon]: showColon }]">:</span>
<span v-if="showMs" v-text="ms"></span>
</span>
</template>
@@ -62,16 +62,14 @@ onUnmounted(() => {
});
</script>
<style lang="scss" scoped>
.zjobosdg {
> .colon {
opacity: 0;
transition: opacity 1s ease;
<style lang="scss" module>
.colon {
opacity: 0;
transition: opacity 1s ease;
&.showColon {
opacity: 1;
transition: opacity 0s;
}
&.showColon {
opacity: 1;
transition: opacity 0s;
}
}
</style>

View File

@@ -1,17 +1,20 @@
<template>
<div ref="rootEl" class="dwzlatin" :class="{ opened }">
<div class="header _button" @click="toggle">
<span class="icon"><slot name="icon"></slot></span>
<span class="text"><slot name="label"></slot></span>
<span class="right">
<span class="text"><slot name="suffix"></slot></span>
<div ref="rootEl" :class="[$style.root, { [$style.opened]: opened }]">
<div :class="$style.header" class="_button" @click="toggle">
<span :class="$style.headerIcon"><slot name="icon"></slot></span>
<span :class="$style.headerText"><slot name="label"></slot></span>
<span :class="$style.headerRight">
<span :class="$style.headerRightText"><slot name="suffix"></slot></span>
<i v-if="opened" class="ti ti-chevron-up icon"></i>
<i v-else class="ti ti-chevron-down icon"></i>
</span>
</div>
<div v-if="openedAtLeastOnce" class="body" :class="{ bgSame }" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null }">
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null }">
<Transition
:name="$store.state.animation ? 'folder-toggle' : ''"
:enter-active-class="$store.state.animation ? $style.transition_toggle_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_toggle_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_toggle_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_toggle_leaveTo : ''"
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
@@ -94,85 +97,88 @@ onMounted(() => {
});
</script>
<style lang="scss" scoped>
.folder-toggle-enter-active, .folder-toggle-leave-active {
<style lang="scss" module>
.transition_toggle_enterActive,
.transition_toggle_leaveActive {
overflow-y: clip;
transition: opacity 0.3s, height 0.3s, transform 0.3s !important;
}
.folder-toggle-enter-from, .folder-toggle-leave-to {
.transition_toggle_enterFrom,
.transition_toggle_leaveTo {
opacity: 0;
}
.dwzlatin {
.root {
display: block;
> .header {
display: flex;
align-items: center;
width: 100%;
box-sizing: border-box;
padding: 10px 14px 10px 14px;
background: var(--buttonBg);
border-radius: 6px;
&:hover {
text-decoration: none;
background: var(--buttonHoverBg);
}
&.active {
color: var(--accent);
background: var(--buttonHoverBg);
}
> .icon {
margin-right: 0.75em;
flex-shrink: 0;
text-align: center;
opacity: 0.8;
&:empty {
display: none;
& + .text {
padding-left: 4px;
}
}
}
> .text {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
padding-right: 12px;
}
> .right {
margin-left: auto;
opacity: 0.7;
white-space: nowrap;
> .text:not(:empty) {
margin-right: 0.75em;
}
}
}
> .body {
background: var(--panel);
border-radius: 0 0 6px 6px;
container-type: inline-size;
overflow: auto;
&.bgSame {
background: var(--bg);
}
}
&.opened {
> .header {
border-radius: 6px 6px 0 0;
}
}
}
.header {
display: flex;
align-items: center;
width: 100%;
box-sizing: border-box;
padding: 9px 12px 9px 12px;
background: var(--buttonBg);
border-radius: 6px;
transition: border-radius 0.3s;
&:hover {
text-decoration: none;
background: var(--buttonHoverBg);
}
&.active {
color: var(--accent);
background: var(--buttonHoverBg);
}
}
.headerIcon {
margin-right: 0.75em;
flex-shrink: 0;
text-align: center;
opacity: 0.8;
&:empty {
display: none;
& + .headerText {
padding-left: 4px;
}
}
}
.headerText {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
padding-right: 12px;
}
.headerRight {
margin-left: auto;
opacity: 0.7;
white-space: nowrap;
}
.headerRightText:not(:empty) {
margin-right: 0.75em;
}
.body {
background: var(--panel);
border-radius: 0 0 6px 6px;
container-type: inline-size;
overflow: auto;
&.bgSame {
background: var(--bg);
}
}
</style>

View File

@@ -1,8 +1,15 @@
<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" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
<div ref="content" class="content" :class="{ fixed, top: type === 'dialog:top' }" :style="{ zIndex }" @click.self="onBgClick">
<Transition
:name="transitionName"
:enter-active-class="$style['transition_' + transitionName + '_enterActive']"
:leave-active-class="$style['transition_' + transitionName + '_leaveActive']"
:enter-from-class="$style['transition_' + transitionName + '_enterFrom']"
:leave-to-class="$style['transition_' + transitionName + '_leaveTo']"
: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="[$style.root, { [$style.drawer]: type === 'drawer', [$style.dialog]: type === 'dialog' || type === 'dialog:top', [$style.popup]: type === 'popup' }]" :style="{ zIndex, pointerEvents: (manualShowing != null ? manualShowing : showing) ? 'auto' : 'none', '--transformOrigin': transformOrigin }">
<div class="_modalBg" :class="[$style.bg, { [$style.bgTransparent]: transparentBg && (type === 'popup') }]" :style="{ zIndex }" @click="onBgClick" @mousedown="onBgClick" @contextmenu.prevent.stop="() => {}"></div>
<div ref="content" :class="[$style.content, { [$style.fixed]: fixed, [$style.top]: type === 'dialog:top' }]" :style="{ zIndex }" @click.self="onBgClick">
<slot :max-height="maxHeight" :type="type"></slot>
</div>
</div>
@@ -280,8 +287,9 @@ defineExpose({
});
</script>
<style lang="scss" scoped>
.send-enter-active, .send-leave-active {
<style lang="scss" module>
.transition_send_enterActive,
.transition_send_leaveActive {
> .bg {
transition: opacity 0.3s !important;
}
@@ -291,7 +299,8 @@ defineExpose({
transition: opacity 0.3s ease-in, transform 0.3s cubic-bezier(.5,-0.5,1,.5) !important;
}
}
.send-enter-from, .send-leave-to {
.transition_send_enterFrom,
.transition_send_leaveTo {
> .bg {
opacity: 0;
}
@@ -303,7 +312,8 @@ defineExpose({
}
}
.modal-enter-active, .modal-leave-active {
.transition_modal_enterActive,
.transition_modal_leaveActive {
> .bg {
transition: opacity 0.2s !important;
}
@@ -313,7 +323,8 @@ defineExpose({
transition: opacity 0.2s, transform 0.2s !important;
}
}
.modal-enter-from, .modal-leave-to {
.transition_modal_enterFrom,
.transition_modal_leaveTo {
> .bg {
opacity: 0;
}
@@ -326,7 +337,8 @@ defineExpose({
}
}
.modal-popup-enter-active, .modal-popup-leave-active {
.transition_modal-popup_enterActive,
.transition_modal-popup_leaveActive {
> .bg {
transition: opacity 0.1s !important;
}
@@ -336,7 +348,8 @@ defineExpose({
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 {
.transition_modal-popup_enterFrom,
.transition_modal-popup_leaveTo {
> .bg {
opacity: 0;
}
@@ -349,7 +362,7 @@ defineExpose({
}
}
.modal-drawer-enter-active {
.transition_modal-drawer_enterActive {
> .bg {
transition: opacity 0.2s !important;
}
@@ -358,7 +371,7 @@ defineExpose({
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
}
}
.modal-drawer-leave-active {
.transition_modal-drawer_leaveActive {
> .bg {
transition: opacity 0.2s !important;
}
@@ -367,7 +380,8 @@ defineExpose({
transition: transform 0.2s cubic-bezier(0,.5,0,1) !important;
}
}
.modal-drawer-enter-from, .modal-drawer-leave-to {
.transition_modal-drawer_enterFrom,
.transition_modal-drawer_leaveTo {
> .bg {
opacity: 0;
}
@@ -378,15 +392,7 @@ defineExpose({
}
}
.qzhlnise {
> .bg {
&.transparent {
background: transparent;
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
}
.root {
&.dialog {
> .content {
position: fixed;
@@ -408,12 +414,12 @@ defineExpose({
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%);
}
> ::v-deep(*) {
&:global > * {
margin: auto;
}
&.top {
> ::v-deep(*) {
&:global > * {
margin-top: 0;
}
}
@@ -445,11 +451,18 @@ defineExpose({
right: 0;
margin: auto;
> ::v-deep(*) {
&:global > * {
margin: auto;
}
}
}
}
.bg {
&.bgTransparent {
background: transparent;
-webkit-backdrop-filter: none;
backdrop-filter: none;
}
}
</style>

View File

@@ -1,6 +1,10 @@
<template>
<MkA v-adaptive-bg :to="`/admin/roles/${role.id}`" class="_panel" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
<div :class="$style.title">{{ role.name }}</div>
<div :class="$style.title">
<span :class="$style.name">{{ role.name }}</span>
<span v-if="role.target === 'manual'" :class="$style.users">{{ role.usersCount }} users</span>
<span v-else-if="role.target === 'conditional'" :class="$style.users">({{ i18n.ts._role.conditional }})</span>
</div>
<div :class="$style.description">{{ role.description }}</div>
</MkA>
</template>
@@ -9,6 +13,7 @@
import { } from 'vue';
import * as misskey from 'misskey-js';
import * as os from '@/os';
import { i18n } from '@/i18n';
const props = defineProps<{
role: any;
@@ -23,9 +28,18 @@ const props = defineProps<{
}
.title {
display: flex;
}
.name {
font-weight: bold;
}
.users {
margin-left: auto;
opacity: 0.7;
}
.description {
opacity: 0.7;
}

View File

@@ -1,8 +1,14 @@
<template>
<div class="mk-toast">
<Transition :name="$store.state.animation ? 'toast' : ''" appear @after-leave="emit('closed')">
<div v-if="showing" class="body _acrylic" :style="{ zIndex }">
<div class="message">
<div>
<Transition
:enter-active-class="$store.state.animation ? $style.transition_toast_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_toast_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_toast_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_toast_leaveTo : ''"
appear @after-leave="emit('closed')"
>
<div v-if="showing" class="_acrylic" :class="$style.root" :style="{ zIndex }">
<div style="padding: 16px 24px;">
{{ message }}
</div>
</div>
@@ -32,35 +38,31 @@ onMounted(() => {
});
</script>
<style lang="scss" scoped>
.toast-enter-active, .toast-leave-active {
<style lang="scss" module>
.transition_toast_enterActive,
.transition_toast_leaveActive {
transition: opacity 0.3s, transform 0.3s !important;
}
.toast-enter-from, .toast-leave-to {
.transition_toast_enterFrom,
.transition_toast_leaveTo {
opacity: 0;
transform: translateY(-100%);
}
.mk-toast {
> .body {
position: fixed;
left: 0;
right: 0;
top: 0;
margin: 0 auto;
margin-top: 16px;
min-width: 300px;
max-width: calc(100% - 32px);
width: min-content;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
border-radius: 8px;
overflow: clip;
text-align: center;
pointer-events: none;
> .message {
padding: 16px 24px;
}
}
> .root {
position: fixed;
left: 0;
right: 0;
top: 0;
margin: 0 auto;
margin-top: 16px;
min-width: 300px;
max-width: calc(100% - 32px);
width: min-content;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
border-radius: 8px;
overflow: clip;
text-align: center;
pointer-events: none;
}
</style>

View File

@@ -1,10 +1,10 @@
<template>
<MkModal ref="modal" :z-priority="'middle'" @click="$refs.modal.close()" @closed="$emit('closed')">
<div class="ewlycnyt">
<div class="title"><MkSparkle>{{ i18n.ts.misskeyUpdated }}</MkSparkle></div>
<div class="version">{{ version }}🚀</div>
<div :class="$style.root">
<div :class="$style.title"><MkSparkle>{{ i18n.ts.misskeyUpdated }}</MkSparkle></div>
<div :class="$style.version">{{ version }}🚀</div>
<MkButton full @click="whatIsNew">{{ i18n.ts.whatIsNew }}</MkButton>
<MkButton class="gotIt" primary full @click="$refs.modal.close()">{{ i18n.ts.gotIt }}</MkButton>
<MkButton :class="$style.gotIt" primary full @click="$refs.modal.close()">{{ i18n.ts.gotIt }}</MkButton>
</div>
</MkModal>
</template>
@@ -32,8 +32,8 @@ onMounted(() => {
});
</script>
<style lang="scss" scoped>
.ewlycnyt {
<style lang="scss" module>
.root {
position: relative;
padding: 32px;
min-width: 320px;
@@ -42,17 +42,17 @@ onMounted(() => {
text-align: center;
background: var(--panel);
border-radius: var(--radius);
}
> .title {
font-weight: bold;
}
.title {
font-weight: bold;
}
> .version {
margin: 1em 0;
}
.version {
margin: 1em 0;
}
> .gotIt {
margin: 8px 0 0 0;
}
.gotIt {
margin: 8px 0 0 0;
}
</style>

View File

@@ -1,38 +1,38 @@
<template>
<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
<button class="disablePlayer" :title="i18n.ts.disablePlayer" @click="playerEnabled = false"><i class="ti ti-x"></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 v-if="playerEnabled" :class="$style.player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`">
<button :class="$style.disablePlayer" :title="i18n.ts.disablePlayer" @click="playerEnabled = false"><i class="ti ti-x"></i></button>
<iframe :class="$style.playerIframe" :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="$style.twitter">
<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}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
</div>
<div v-else class="mk-url-preview">
<component :is="self ? 'MkA' : 'a'" 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}')`">
<div v-else :class="$style.urlPreview">
<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
<div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`">
</div>
<article>
<header>
<h1 v-if="unknownUrl">{{ url }}</h1>
<h1 v-else-if="fetching"><MkEllipsis/></h1>
<h1 v-else :title="title">{{ title }}</h1>
<article :class="$style.body">
<header :class="$style.header">
<h1 v-if="unknownUrl" :class="$style.title">{{ url }}</h1>
<h1 v-else-if="fetching" :class="$style.title"><MkEllipsis/></h1>
<h1 v-else :class="$style.title" :title="title">{{ title }}</h1>
</header>
<p v-if="unknownUrl">{{ i18n.ts.cannotLoad }}</p>
<p v-else-if="fetching"><MkEllipsis/></p>
<p v-else-if="description" :title="description">{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p>
<footer>
<img v-if="icon" class="icon" :src="icon"/>
<p v-if="unknownUrl">?</p>
<p v-else-if="fetching"><MkEllipsis/></p>
<p v-else :title="sitename">{{ sitename }}</p>
<p v-if="unknownUrl" :class="$style.text">{{ i18n.ts.cannotLoad }}</p>
<p v-else-if="fetching" :class="$style.text"><MkEllipsis/></p>
<p v-else-if="description" :class="$style.text" :title="description">{{ description.length > 85 ? description.slice(0, 85) + '…' : description }}</p>
<footer :class="$style.footer">
<img v-if="icon" :class="$style.siteIcon" :src="icon"/>
<p v-if="unknownUrl" :class="$style.siteName">?</p>
<p v-else-if="fetching" :class="$style.siteName"><MkEllipsis/></p>
<p v-else :class="$style.siteName" :title="sitename">{{ sitename }}</p>
</footer>
</article>
</component>
<div v-if="tweetId" class="action">
<div v-if="tweetId" :class="$style.action">
<MkButton :small="true" inline @click="tweetExpanded = true">
<i class="ti ti-brand-twitter"></i> {{ i18n.ts.expandTweet }}
</MkButton>
</div>
<div v-if="!playerEnabled && player.url" class="action">
<div v-if="!playerEnabled && player.url" :class="$style.action">
<MkButton :small="true" inline @click="playerEnabled = true">
<i class="ti ti-player-play"></i> {{ i18n.ts.enablePlayer }}
</MkButton>
@@ -136,197 +136,198 @@ onUnmounted(() => {
});
</script>
<style lang="scss" scoped>
<style lang="scss" module>
.player {
position: relative;
width: 100%;
}
> button {
position: absolute;
top: -1.5em;
right: 0;
font-size: 1em;
width: 1.5em;
height: 1.5em;
padding: 0;
margin: 0;
color: var(--fg);
background: rgba(128, 128, 128, 0.2);
opacity: 0.7;
.disablePlayer {
position: absolute;
top: -1.5em;
right: 0;
font-size: 1em;
width: 1.5em;
height: 1.5em;
padding: 0;
margin: 0;
color: var(--fg);
background: rgba(128, 128, 128, 0.2);
opacity: 0.7;
&:hover {
opacity: 0.9;
}
}
> iframe {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
&:hover {
opacity: 0.9;
}
}
.mk-url-preview {
> .link {
position: relative;
display: block;
font-size: 14px;
box-shadow: 0 0 0 1px var(--divider);
border-radius: 8px;
overflow: clip;
.playerIframe {
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
&:hover {
text-decoration: none;
border-color: rgba(0, 0, 0, 0.2);
.twitter {
> article > header > h1 {
text-decoration: underline;
}
}
}
> .thumbnail {
position: absolute;
width: 100px;
height: 100%;
background-position: center;
background-size: cover;
display: flex;
justify-content: center;
align-items: center;
.urlPreview {
}
& + article {
left: 100px;
width: calc(100% - 100px);
}
}
.link {
position: relative;
display: block;
font-size: 14px;
box-shadow: 0 0 0 1px var(--divider);
border-radius: 8px;
overflow: clip;
> article {
position: relative;
box-sizing: border-box;
padding: 16px;
&:hover {
text-decoration: none;
border-color: rgba(0, 0, 0, 0.2);
> header {
margin-bottom: 8px;
> h1 {
margin: 0;
font-size: 1em;
}
}
> p {
margin: 0;
font-size: 0.8em;
}
> footer {
margin-top: 8px;
height: 16px;
> img {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 4px;
vertical-align: top;
}
> p {
display: inline-block;
margin: 0;
color: var(--urlPreviewInfo);
font-size: 0.8em;
line-height: 16px;
vertical-align: top;
}
}
}
&.compact {
> article {
> header h1, p, footer {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
> .body > .header > .title {
text-decoration: underline;
}
}
> .action {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-top: 6px;
&.compact {
> .body {
> .header .title, .text, .footer {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
}
.thumbnail {
position: absolute;
width: 100px;
height: 100%;
background-position: center;
background-size: cover;
display: flex;
justify-content: center;
align-items: center;
& + .body {
left: 100px;
width: calc(100% - 100px);
}
}
.body {
position: relative;
box-sizing: border-box;
padding: 16px;
}
.header {
margin-bottom: 8px;
}
.title {
margin: 0;
font-size: 1em;
}
.text {
margin: 0;
font-size: 0.8em;
}
.footer {
margin-top: 8px;
height: 16px;
}
.siteIcon {
display: inline-block;
width: 16px;
height: 16px;
margin-right: 4px;
vertical-align: top;
}
.siteName {
display: inline-block;
margin: 0;
color: var(--urlPreviewInfo);
font-size: 0.8em;
line-height: 16px;
vertical-align: top;
}
.action {
display: flex;
gap: 6px;
flex-wrap: wrap;
margin-top: 6px;
}
@container (max-width: 400px) {
.mk-url-preview {
> .link {
font-size: 12px;
.link {
font-size: 12px;
}
> .thumbnail {
height: 80px;
}
.thumbnail {
height: 80px;
}
> article {
padding: 12px;
}
}
.body {
padding: 12px;
}
}
@container (max-width: 350px) {
.mk-url-preview {
> .link {
font-size: 10px;
.link {
font-size: 10px;
&.compact {
> .thumbnail {
height: 70px;
position: absolute;
width: 56px;
height: 100%;
}
> article {
padding: 8px;
> .body {
left: 56px;
width: calc(100% - 56px);
padding: 4px;
> header {
margin-bottom: 4px;
> .header {
margin-bottom: 2px;
}
> footer {
margin-top: 4px;
> img {
width: 12px;
height: 12px;
}
}
}
&.compact {
> .thumbnail {
position: absolute;
width: 56px;
height: 100%;
}
> article {
left: 56px;
width: calc(100% - 56px);
padding: 4px;
> header {
margin-bottom: 2px;
}
> footer {
margin-top: 2px;
}
> .footer {
margin-top: 2px;
}
}
}
}
.thumbnail {
height: 70px;
}
.body {
padding: 8px;
}
.header {
margin-bottom: 4px;
}
.footer {
margin-top: 4px;
}
.siteIcon {
width: 12px;
height: 12px;
}
}
</style>

View File

@@ -1,11 +1,11 @@
<template>
<MkTooltip ref="tooltip" :showing="showing" :target-element="targetElement" :max-width="250" @closed="emit('closed')">
<div class="beaffaef">
<div v-for="u in users" :key="u.id" class="user">
<MkAvatar class="avatar" :user="u"/>
<MkUserName class="name" :user="u" :nowrap="true"/>
<div :class="$style.root">
<div v-for="u in users" :key="u.id" :class="$style.user">
<MkAvatar :class="$style.avatar" :user="u"/>
<MkUserName :class="$style.name" :user="u" :nowrap="true"/>
</div>
<div v-if="users.length < count" class="omitted">+{{ count - users.length }}</div>
<div v-if="users.length < count" :class="$style.omitted">+{{ count - users.length }}</div>
</div>
</MkTooltip>
</template>
@@ -26,26 +26,34 @@ const emit = defineEmits<{
}>();
</script>
<style lang="scss" scoped>
.beaffaef {
<style lang="scss" module>
.root {
font-size: 0.9em;
text-align: left;
}
> .user {
line-height: 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.user {
line-height: 24px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:not(:last-child) {
margin-bottom: 3px;
}
> .avatar {
width: 24px;
height: 24px;
margin-right: 3px;
}
&:not(:last-child) {
margin-bottom: 3px;
}
}
.name {
}
.omitted {
}
.avatar {
width: 24px;
height: 24px;
margin-right: 3px;
}
</style>

View File

@@ -1,34 +1,41 @@
<template>
<Transition :name="$store.state.animation ? 'window' : ''" appear @after-leave="$emit('closed')">
<div v-if="showing" ref="rootEl" class="ebkgocck" :class="{ maximized }">
<div class="body _shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
<div class="header" :class="{ mini }" @contextmenu.prevent.stop="onContextmenu">
<span class="left">
<button v-for="button in buttonsLeft" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
<Transition
:enter-active-class="$store.state.animation ? $style.transition_window_enterActive : ''"
:leave-active-class="$store.state.animation ? $style.transition_window_leaveActive : ''"
:enter-from-class="$store.state.animation ? $style.transition_window_enterFrom : ''"
:leave-to-class="$store.state.animation ? $style.transition_window_leaveTo : ''"
appear
@after-leave="$emit('closed')"
>
<div v-if="showing" ref="rootEl" :class="[$style.root, { [$style.maximized]: maximized }]">
<div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
<div :class="[$style.header, { [$style.mini]: mini }]" @contextmenu.prevent.stop="onContextmenu">
<span :class="$style.headerLeft">
<button v-for="button in buttonsLeft" v-tooltip="button.title" class="_button" :class="[$style.headerButton, { [$style.highlighted]: button.highlighted }]" @click="button.onClick"><i :class="button.icon"></i></button>
</span>
<span class="title" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
<span :class="$style.headerTitle" @mousedown.prevent="onHeaderMousedown" @touchstart.prevent="onHeaderMousedown">
<slot name="header"></slot>
</span>
<span class="right">
<button v-for="button in buttonsRight" v-tooltip="button.title" class="button _button" :class="{ highlighted: button.highlighted }" @click="button.onClick"><i :class="button.icon"></i></button>
<button v-if="canResize && maximized" v-tooltip="i18n.ts.windowRestore" class="button _button" @click="unMaximize()"><i class="ti ti-picture-in-picture"></i></button>
<button v-else-if="canResize && !maximized" v-tooltip="i18n.ts.windowMaximize" class="button _button" @click="maximize()"><i class="ti ti-rectangle"></i></button>
<button v-if="closeButton" v-tooltip="i18n.ts.close" class="button _button" @click="close()"><i class="ti ti-x"></i></button>
<span :class="$style.headerRight">
<button v-for="button in buttonsRight" v-tooltip="button.title" class="_button" :class="[$style.headerButton, { [$style.highlighted]: button.highlighted }]" @click="button.onClick"><i :class="button.icon"></i></button>
<button v-if="canResize && maximized" v-tooltip="i18n.ts.windowRestore" class="_button" :class="$style.headerButton" @click="unMaximize()"><i class="ti ti-picture-in-picture"></i></button>
<button v-else-if="canResize && !maximized" v-tooltip="i18n.ts.windowMaximize" class="_button" :class="$style.headerButton" @click="maximize()"><i class="ti ti-rectangle"></i></button>
<button v-if="closeButton" v-tooltip="i18n.ts.close" class="_button" :class="$style.headerButton" @click="close()"><i class="ti ti-x"></i></button>
</span>
</div>
<div class="body">
<div :class="$style.content">
<slot></slot>
</div>
</div>
<template v-if="canResize">
<div class="handle top" @mousedown.prevent="onTopHandleMousedown"></div>
<div class="handle right" @mousedown.prevent="onRightHandleMousedown"></div>
<div class="handle bottom" @mousedown.prevent="onBottomHandleMousedown"></div>
<div class="handle left" @mousedown.prevent="onLeftHandleMousedown"></div>
<div class="handle top-left" @mousedown.prevent="onTopLeftHandleMousedown"></div>
<div class="handle top-right" @mousedown.prevent="onTopRightHandleMousedown"></div>
<div class="handle bottom-right" @mousedown.prevent="onBottomRightHandleMousedown"></div>
<div class="handle bottom-left" @mousedown.prevent="onBottomLeftHandleMousedown"></div>
<div :class="$style.handleTop" @mousedown.prevent="onTopHandleMousedown"></div>
<div :class="$style.handleRight" @mousedown.prevent="onRightHandleMousedown"></div>
<div :class="$style.handleBottom" @mousedown.prevent="onBottomHandleMousedown"></div>
<div :class="$style.handleLeft" @mousedown.prevent="onLeftHandleMousedown"></div>
<div :class="$style.handleTopLeft" @mousedown.prevent="onTopLeftHandleMousedown"></div>
<div :class="$style.handleTopRight" @mousedown.prevent="onTopRightHandleMousedown"></div>
<div :class="$style.handleBottomRight" @mousedown.prevent="onBottomRightHandleMousedown"></div>
<div :class="$style.handleBottomLeft" @mousedown.prevent="onBottomLeftHandleMousedown"></div>
</template>
</div>
</Transition>
@@ -407,166 +414,174 @@ defineExpose({
});
</script>
<style lang="scss" scoped>
.window-enter-active, .window-leave-active {
<style lang="scss" module>
.transition_window_enterActive,
.transition_window_leaveActive {
transition: opacity 0.2s, transform 0.2s !important;
}
.window-enter-from, .window-leave-to {
.transition_window_enterFrom,
.transition_window_leaveTo {
pointer-events: none;
opacity: 0;
transform: scale(0.9);
}
.ebkgocck {
.root {
position: fixed;
top: 0;
left: 0;
> .body {
overflow: clip;
display: flex;
flex-direction: column;
contain: content;
width: 100%;
height: 100%;
border-radius: var(--radius);
> .header {
--height: 39px;
&.mini {
--height: 32px;
}
display: flex;
position: relative;
z-index: 1;
flex-shrink: 0;
user-select: none;
height: var(--height);
background: var(--windowHeader);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
//border-bottom: solid 1px var(--divider);
font-size: 95%;
font-weight: bold;
> .left, > .right {
> .button {
height: var(--height);
width: var(--height);
&:hover {
color: var(--fgHighlighted);
}
&.highlighted {
color: var(--accent);
}
}
}
> .left {
margin-right: 16px;
}
> .right {
min-width: 16px;
}
> .title {
flex: 1;
position: relative;
line-height: var(--height);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: move;
}
}
> .body {
flex: 1;
overflow: auto;
background: var(--panel);
container-type: inline-size;
}
}
> .handle {
$size: 8px;
position: absolute;
&.top {
top: -($size);
left: 0;
width: 100%;
height: $size;
cursor: ns-resize;
}
&.right {
top: 0;
right: -($size);
width: $size;
height: 100%;
cursor: ew-resize;
}
&.bottom {
bottom: -($size);
left: 0;
width: 100%;
height: $size;
cursor: ns-resize;
}
&.left {
top: 0;
left: -($size);
width: $size;
height: 100%;
cursor: ew-resize;
}
&.top-left {
top: -($size);
left: -($size);
width: $size * 2;
height: $size * 2;
cursor: nwse-resize;
}
&.top-right {
top: -($size);
right: -($size);
width: $size * 2;
height: $size * 2;
cursor: nesw-resize;
}
&.bottom-right {
bottom: -($size);
right: -($size);
width: $size * 2;
height: $size * 2;
cursor: nwse-resize;
}
&.bottom-left {
bottom: -($size);
left: -($size);
width: $size * 2;
height: $size * 2;
cursor: nesw-resize;
}
}
&.maximized {
> .body {
border-radius: 0;
}
}
}
.body {
overflow: clip;
display: flex;
flex-direction: column;
contain: content;
width: 100%;
height: 100%;
border-radius: var(--radius);
}
.header {
--height: 39px;
&.mini {
--height: 32px;
}
display: flex;
position: relative;
z-index: 1;
flex-shrink: 0;
user-select: none;
height: var(--height);
background: var(--windowHeader);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
//border-bottom: solid 1px var(--divider);
font-size: 95%;
font-weight: bold;
}
.headerButton {
height: var(--height);
width: var(--height);
&:hover {
color: var(--fgHighlighted);
}
&.highlighted {
color: var(--accent);
}
}
.headerLeft {
margin-right: 16px;
}
.headerRight {
min-width: 16px;
}
.headerTitle {
flex: 1;
position: relative;
line-height: var(--height);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: move;
}
.content {
flex: 1;
overflow: auto;
background: var(--panel);
container-type: inline-size;
}
$handleSize: 8px;
.handle {
position: absolute;
}
.handleTop {
composes: handle;
top: -($handleSize);
left: 0;
width: 100%;
height: $handleSize;
cursor: ns-resize;
}
.handleRight {
composes: handle;
top: 0;
right: -($handleSize);
width: $handleSize;
height: 100%;
cursor: ew-resize;
}
.handleBottom {
composes: handle;
bottom: -($handleSize);
left: 0;
width: 100%;
height: $handleSize;
cursor: ns-resize;
}
.handleLeft {
composes: handle;
top: 0;
left: -($handleSize);
width: $handleSize;
height: 100%;
cursor: ew-resize;
}
.handleTopLeft {
composes: handle;
top: -($handleSize);
left: -($handleSize);
width: $handleSize * 2;
height: $handleSize * 2;
cursor: nwse-resize;
}
.handleTopRight {
composes: handle;
top: -($handleSize);
right: -($handleSize);
width: $handleSize * 2;
height: $handleSize * 2;
cursor: nesw-resize;
}
.handleBottomRight {
composes: handle;
bottom: -($handleSize);
right: -($handleSize);
width: $handleSize * 2;
height: $handleSize * 2;
cursor: nwse-resize;
}
.handleBottomLeft {
composes: handle;
bottom: -($handleSize);
left: -($handleSize);
width: $handleSize * 2;
height: $handleSize * 2;
cursor: nesw-resize;
}
</style>

View File

@@ -1,16 +1,16 @@
<template>
<div v-if="chosen" class="qiivuoyo">
<div v-if="!showMenu" class="main" :class="chosen.place">
<a :href="chosen.url" target="_blank">
<img :src="chosen.imageUrl">
<button class="_button menu" @click.prevent.stop="toggleMenu"><span class="ti ti-info-circle info-circle"></span></button>
<div v-if="chosen" :class="$style.root">
<div v-if="!showMenu" :class="[$style.main, $style['form_' + chosen.place]]">
<a :href="chosen.url" target="_blank" :class="$style.link">
<img :src="chosen.imageUrl" :class="$style.img">
<button class="_button" :class="$style.i" @click.prevent.stop="toggleMenu"><i :class="$style.iIcon" class="ti ti-info-circle"></i></button>
</a>
</div>
<div v-else class="menu">
<div class="body">
<div v-else :class="$style.menu">
<div :class="$style.menuContainer">
<div>Ads by {{ host }}</div>
<!--<MkButton class="button" primary>{{ $ts._ad.like }}</MkButton>-->
<MkButton v-if="chosen.ratio !== 0" class="button" @click="reduceFrequency">{{ $ts._ad.reduceFrequencyOfThisAd }}</MkButton>
<MkButton v-if="chosen.ratio !== 0" :class="$style.menuButton" @click="reduceFrequency">{{ $ts._ad.reduceFrequencyOfThisAd }}</MkButton>
<button class="_textButton" @click="toggleMenu">{{ $ts._ad.back }}</button>
</div>
</div>
@@ -92,95 +92,99 @@ function reduceFrequency(): void {
}
</script>
<style lang="scss" scoped>
.qiivuoyo {
<style lang="scss" module>
.root {
background-size: auto auto;
background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px );
}
> .main {
text-align: center;
.main {
text-align: center;
> a {
display: inline-block;
position: relative;
vertical-align: bottom;
&:hover {
> img {
filter: contrast(120%);
}
}
> img {
display: block;
object-fit: contain;
margin: auto;
border-radius: 5px;
}
> .menu {
position: absolute;
top: 1px;
right: 1px;
> .info-circle {
border: 3px solid var(--panel);
border-radius: 50%;
background: var(--panel);
}
}
}
&.square {
> a ,
> a > img {
max-width: min(300px, 100%);
max-height: 300px;
}
}
&.horizontal {
padding: 8px;
> a ,
> a > img {
max-width: min(600px, 100%);
max-height: 80px;
}
}
&.horizontal-big {
padding: 8px;
> a ,
> a > img {
max-width: min(600px, 100%);
max-height: 250px;
}
}
&.vertical {
> a ,
> a > img {
max-width: min(100px, 100%);
}
&.form_square {
> .link,
> .link > .img {
max-width: min(300px, 100%);
max-height: 300px;
}
}
> .menu {
&.form_horizontal {
padding: 8px;
text-align: center;
> .body {
padding: 8px;
margin: 0 auto;
max-width: 400px;
border: solid 1px var(--divider);
> .link,
> .link > .img {
max-width: min(600px, 100%);
max-height: 80px;
}
}
> .button {
margin: 8px auto;
}
&.form_horizontal-big {
padding: 8px;
> .link,
> .link > .img {
max-width: min(600px, 100%);
max-height: 250px;
}
}
&.form_vertical {
> .link,
> .link > .img {
max-width: min(100px, 100%);
}
}
}
.link {
display: inline-block;
position: relative;
vertical-align: bottom;
&:hover {
> .img {
filter: contrast(120%);
}
}
}
.img {
display: block;
object-fit: contain;
margin: auto;
border-radius: 5px;
}
.i {
position: absolute;
top: 1px;
right: 1px;
display: grid;
place-content: center;
background: var(--panel);
border-radius: 100%;
padding: 2px;
}
.iIcon {
font-size: 14px;
line-height: 17px;
}
.menu {
padding: 8px;
text-align: center;
}
.menuContainer {
padding: 8px;
margin: 0 auto;
max-width: 400px;
border: solid 1px var(--divider);
}
.menuButton {
margin: 8px auto;
}
</style>

View File

@@ -1,28 +1,10 @@
<template>
<span class="mk-ellipsis">
<span>.</span><span>.</span><span>.</span>
</span>
<span :class="$style.root">
<span :class="$style.dot">.</span><span :class="$style.dot">.</span><span :class="$style.dot">.</span>
</span>
</template>
<style lang="scss" scoped>
.mk-ellipsis {
> span {
animation: ellipsis 1.4s infinite ease-in-out both;
&:nth-child(1) {
animation-delay: 0s;
}
&:nth-child(2) {
animation-delay: 0.16s;
}
&:nth-child(3) {
animation-delay: 0.32s;
}
}
}
<style lang="scss" module>
@keyframes ellipsis {
0%, 80%, 100% {
opacity: 1;
@@ -31,4 +13,24 @@
opacity: 0;
}
}
.root {
}
.dot {
animation: ellipsis 1.4s infinite ease-in-out both;
&:nth-child(1) {
animation-delay: 0s;
}
&:nth-child(2) {
animation-delay: 0.16s;
}
&:nth-child(3) {
animation-delay: 0.32s;
}
}
</style>

View File

@@ -1,9 +1,9 @@
<template>
<Transition :name="$store.state.animation ? '_transition_zoom' : ''" appear>
<div class="mjndxjcg">
<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
<p><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p>
<MkButton class="button" @click="() => $emit('retry')">{{ i18n.ts.retry }}</MkButton>
<div :class="$style.root">
<img :class="$style.img" src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
<p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p>
<MkButton :class="$style.button" @click="() => $emit('retry')">{{ i18n.ts.retry }}</MkButton>
</div>
</Transition>
</template>
@@ -13,24 +13,24 @@ import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n';
</script>
<style lang="scss" scoped>
.mjndxjcg {
<style lang="scss" module>
.root {
padding: 32px;
text-align: center;
}
> p {
margin: 0 0 8px 0;
}
.text {
margin: 0 0 8px 0;
}
> .button {
margin: 0 auto;
}
.button {
margin: 0 auto;
}
> img {
vertical-align: bottom;
height: 128px;
margin-bottom: 16px;
border-radius: 16px;
}
.img {
vertical-align: bottom;
height: 128px;
margin-bottom: 16px;
border-radius: 16px;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :is-note="isNote" class="havbbuyv" :class="{ nowrap }"/>
<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :is-note="isNote" :class="[$style.root, { [$style.nowrap]: nowrap }]"/>
</template>
<script lang="ts" setup>
@@ -157,8 +157,8 @@ const props = withDefaults(defineProps<{
}
</style>
<style lang="scss" scoped>
.havbbuyv {
<style lang="scss" module>
.root {
white-space: pre-wrap;
&.nowrap {
@@ -167,24 +167,5 @@ const props = withDefaults(defineProps<{
overflow: hidden;
text-overflow: ellipsis;
}
::v-deep(.quote) {
display: block;
margin: 8px;
padding: 6px 0 6px 12px;
color: var(--fg);
border-left: solid 3px var(--fg);
opacity: 0.7;
}
::v-deep(pre) {
font-size: 0.8em;
}
> ::v-deep(code) {
font-size: 0.8em;
word-break: break-all;
padding: 4px 6px;
}
}
</style>

View File

@@ -1,36 +1,36 @@
<template>
<div v-if="show" ref="el" class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick">
<div v-if="narrow" class="buttons left">
<MkAvatar v-if="props.displayMyAvatar && $i" class="avatar" :user="$i" :disable-preview="true"/>
<div v-if="show" ref="el" :class="[$style.root, { [$style.slim]: narrow, [$style.thin]: thin_ }]" :style="{ background: bg }" @click="onClick">
<div v-if="narrow" :class="$style.buttonsLeft">
<MkAvatar v-if="props.displayMyAvatar && $i" :class="$style.avatar" :user="$i" :disable-preview="true"/>
</div>
<template v-if="metadata">
<div v-if="!hideTitle" class="titleContainer" @click="showTabsPopup">
<MkAvatar v-if="metadata.avatar" class="avatar" :user="metadata.avatar" :disable-preview="true" :show-indicator="true"/>
<i v-else-if="metadata.icon" class="icon" :class="metadata.icon"></i>
<div v-if="!hideTitle" :class="$style.titleContainer" @click="showTabsPopup">
<MkAvatar v-if="metadata.avatar" :class="$style.titleAvatar" :user="metadata.avatar" :disable-preview="true" :show-indicator="true"/>
<i v-else-if="metadata.icon" :class="[$style.titleIcon, metadata.icon]"></i>
<div class="title">
<MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true" class="title"/>
<div v-else-if="metadata.title" class="title">{{ metadata.title }}</div>
<div v-if="!narrow && metadata.subtitle" class="subtitle">
<div :class="$style.title">
<MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true"/>
<div v-else-if="metadata.title">{{ metadata.title }}</div>
<div v-if="!narrow && metadata.subtitle" :class="$style.subtitle">
{{ metadata.subtitle }}
</div>
<div v-if="narrow && hasTabs" class="subtitle activeTab">
<div v-if="narrow && hasTabs" :class="[$style.subtitle, $style.activeTab]">
{{ tabs.find(tab => tab.key === props.tab)?.title }}
<i class="chevron ti ti-chevron-down"></i>
<i class="ti ti-chevron-down" :class="$style.chevron"></i>
</div>
</div>
</div>
<div v-if="!narrow || hideTitle" class="tabs">
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = (el as HTMLElement)" v-tooltip.noDelay="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
<span v-if="!tab.iconOnly" class="title">{{ tab.title }}</span>
<div v-if="!narrow || hideTitle" :class="$style.tabs">
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = (el as HTMLElement)" v-tooltip.noDelay="tab.title" class="_button" :class="[$style.tab, { [$style.active]: tab.key != null && tab.key === props.tab }]" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
<i v-if="tab.icon" :class="[$style.tabIcon, tab.icon]"></i>
<span v-if="!tab.iconOnly" :class="$style.tabTitle">{{ tab.title }}</span>
</button>
<div ref="tabHighlightEl" class="highlight"></div>
<div ref="tabHighlightEl" :class="$style.tabHighlight"></div>
</div>
</template>
<div class="buttons right">
<div :class="$style.buttonsRight">
<template v-for="action in actions">
<button v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
<button v-tooltip.noDelay="action.text" class="_button" :class="[$style.button, { [$style.highlighted]: action.highlighted }]" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
</template>
</div>
</div>
@@ -178,8 +178,8 @@ onUnmounted(() => {
});
</script>
<style lang="scss" scoped>
.fdidabkb {
<style lang="scss" module>
.root {
--height: 50px;
display: flex;
width: 100%;
@@ -215,154 +215,156 @@ onUnmounted(() => {
}
}
}
}
> .buttons {
--margin: 8px;
display: flex;
align-items: center;
min-width: var(--height);
height: var(--height);
margin: 0 var(--margin);
.buttons {
--margin: 8px;
display: flex;
align-items: center;
min-width: var(--height);
height: var(--height);
margin: 0 var(--margin);
&.left {
margin-right: auto;
&:empty {
width: var(--height);
}
}
> .avatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
}
.buttonsLeft {
composes: buttons;
margin-right: auto;
}
&.right {
margin-left: auto;
}
.buttonsRight {
composes: buttons;
margin-left: auto;
}
&:empty {
width: var(--height);
}
.avatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
> .button {
display: flex;
align-items: center;
justify-content: center;
height: calc(var(--height) - (var(--margin) * 2));
width: calc(var(--height) - (var(--margin) * 2));
box-sizing: border-box;
position: relative;
border-radius: 5px;
.button {
display: flex;
align-items: center;
justify-content: center;
height: calc(var(--height) - (var(--margin) * 2));
width: calc(var(--height) - (var(--margin) * 2));
box-sizing: border-box;
position: relative;
border-radius: 5px;
&:hover {
background: rgba(0, 0, 0, 0.05);
}
&.highlighted {
color: var(--accent);
}
}
> .fullButton {
& + .fullButton {
margin-left: 12px;
}
}
&:hover {
background: rgba(0, 0, 0, 0.05);
}
> .titleContainer {
display: flex;
align-items: center;
max-width: 400px;
overflow: auto;
white-space: nowrap;
text-align: left;
font-weight: bold;
flex-shrink: 0;
margin-left: 24px;
> .avatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
> .icon {
margin-right: 8px;
width: 16px;
text-align: center;
}
> .title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.1;
> .subtitle {
opacity: 0.6;
font-size: 0.8em;
font-weight: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.activeTab {
text-align: center;
> .chevron {
display: inline-block;
margin-left: 6px;
}
}
}
}
&.highlighted {
color: var(--accent);
}
}
> .tabs {
position: relative;
margin-left: 16px;
font-size: 0.8em;
overflow: auto;
white-space: nowrap;
.fullButton {
& + .fullButton {
margin-left: 12px;
}
}
> .tab {
.titleContainer {
display: flex;
align-items: center;
max-width: 400px;
overflow: auto;
white-space: nowrap;
text-align: left;
font-weight: bold;
flex-shrink: 0;
margin-left: 24px;
}
.titleAvatar {
$size: 32px;
display: inline-block;
width: $size;
height: $size;
vertical-align: bottom;
margin: 0 8px;
pointer-events: none;
}
.titleIcon {
margin-right: 8px;
width: 16px;
text-align: center;
}
.title {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 1.1;
}
.subtitle {
opacity: 0.6;
font-size: 0.8em;
font-weight: normal;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&.activeTab {
text-align: center;
> .chevron {
display: inline-block;
position: relative;
padding: 0 10px;
height: 100%;
font-weight: normal;
opacity: 0.7;
&:hover {
opacity: 1;
}
&.active {
opacity: 1;
}
> .icon + .title {
margin-left: 8px;
}
}
> .highlight {
position: absolute;
bottom: 0;
height: 3px;
background: var(--accent);
border-radius: 999px;
transition: all 0.2s ease;
pointer-events: none;
margin-left: 6px;
}
}
}
.tabs {
position: relative;
margin-left: 16px;
font-size: 0.8em;
overflow: auto;
white-space: nowrap;
}
.tab {
display: inline-block;
position: relative;
padding: 0 10px;
height: 100%;
font-weight: normal;
opacity: 0.7;
&:hover {
opacity: 1;
}
&.active {
opacity: 1;
}
}
.tabIcon + .tabTitle {
margin-left: 8px;
}
.tabHighlight {
position: absolute;
bottom: 0;
height: 3px;
background: var(--accent);
border-radius: 999px;
transition: all 0.2s ease;
pointer-events: none;
}
</style>

View File

@@ -12,6 +12,15 @@ import MkA from '@/components/global/MkA.vue';
import { host } from '@/config';
import { MFM_TAGS } from '@/scripts/mfm-tags';
const QUOTE_STYLE = `
display: block;
margin: 8px;
padding: 6px 0 6px 12px;
color: var(--fg);
border-left: solid 3px var(--fg);
opacity: 0.7;
`.split('\n').join(' ');
export default defineComponent({
props: {
text: {
@@ -276,11 +285,11 @@ export default defineComponent({
case 'quote': {
if (!this.nowrap) {
return [h('div', {
class: 'quote',
style: QUOTE_STYLE,
}, genEl(token.children))];
} else {
return [h('span', {
class: 'quote',
style: QUOTE_STYLE,
}, genEl(token.children))];
}
}

View File

@@ -140,6 +140,18 @@
</MkInput>
</div>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.webhookMax }}</template>
<template #suffix>{{ options_webhookLimit_useDefault ? i18n.ts._role.useBaseValue : (options_webhookLimit_value) }}</template>
<div class="_gaps">
<MkSwitch v-model="options_webhookLimit_useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkInput v-model="options_webhookLimit_value" :disabled="options_webhookLimit_useDefault" type="number" :readonly="readonly">
</MkInput>
</div>
</MkFolder>
</div>
</FormSlot>
@@ -209,6 +221,8 @@ let options_antennaLimit_useDefault = $ref(role?.options?.antennaLimit?.useDefau
let options_antennaLimit_value = $ref(role?.options?.antennaLimit?.value ?? 0);
let options_wordMuteLimit_useDefault = $ref(role?.options?.wordMuteLimit?.useDefault ?? true);
let options_wordMuteLimit_value = $ref(role?.options?.wordMuteLimit?.value ?? 0);
let options_webhookLimit_useDefault = $ref(role?.options?.webhookLimit?.useDefault ?? true);
let options_webhookLimit_value = $ref(role?.options?.webhookLimit?.value ?? 0);
if (_DEV_) {
watch($$(condFormula), () => {
@@ -226,6 +240,7 @@ function getOptions() {
driveCapacityMb: { useDefault: options_driveCapacityMb_useDefault, value: options_driveCapacityMb_value },
antennaLimit: { useDefault: options_antennaLimit_useDefault, value: options_antennaLimit_value },
wordMuteLimit: { useDefault: options_wordMuteLimit_useDefault, value: options_wordMuteLimit_value },
webhookLimit: { useDefault: options_webhookLimit_useDefault, value: options_webhookLimit_value },
};
}

View File

@@ -71,6 +71,13 @@
</MkInput>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts._role._options.webhookMax }}</template>
<template #suffix>{{ options_webhookLimit }}</template>
<MkInput v-model="options_webhookLimit" type="number">
</MkInput>
</MkFolder>
<MkButton primary rounded @click="updateBaseRole">{{ i18n.ts.save }}</MkButton>
</div>
</MkFolder>
@@ -111,6 +118,7 @@ let options_canManageCustomEmojis = $ref(instance.baseRole.canManageCustomEmojis
let options_driveCapacityMb = $ref(instance.baseRole.driveCapacityMb);
let options_antennaLimit = $ref(instance.baseRole.antennaLimit);
let options_wordMuteLimit = $ref(instance.baseRole.wordMuteLimit);
let options_webhookLimit = $ref(instance.baseRole.webhookLimit);
async function updateBaseRole() {
await os.apiWithDialog('admin/roles/update-default-role-override', {
@@ -123,6 +131,7 @@ async function updateBaseRole() {
driveCapacityMb: options_driveCapacityMb,
antennaLimit: options_antennaLimit,
wordMuteLimit: options_wordMuteLimit,
webhookLimit: options_webhookLimit,
},
});
}

View File

@@ -9,7 +9,8 @@
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
<div :class="$style.tl">
<XTimeline
ref="tl" :key="src"
ref="tlComponent"
:key="src"
:src="src"
:sound="true"
@queue="queueUpdated"

View File

@@ -65,6 +65,3 @@ defineExpose({
});
*/
</script>
<style lang="scss" scoped>
</style>

View File

@@ -53,6 +53,3 @@ const menu = [{
action: setList,
}];
</script>
<style lang="scss" scoped>
</style>

View File

@@ -8,12 +8,12 @@
<span style="margin-left: 8px;">{{ column.name }}</span>
</template>
<div v-if="disabled" class="iwaalbte">
<p>
<div v-if="disabled" :class="$style.disabled">
<p :class="$style.disabledTitle">
<i class="ti ti-minus-circle"></i>
{{ $t('disabled-timeline.title') }}
</p>
<p class="desc">{{ $t('disabled-timeline.description') }}</p>
<p :class="$style.disabledDescription">{{ $t('disabled-timeline.description') }}</p>
</div>
<XTimeline v-else-if="column.tl" ref="timeline" :key="column.tl" :src="column.tl" @after="() => emit('loaded')"/>
</XColumn>
@@ -80,16 +80,16 @@ const menu = [{
}];
</script>
<style lang="scss" scoped>
.iwaalbte {
<style lang="scss" module>
.disabled {
text-align: center;
}
> p {
margin: 16px;
.disabledTitle {
margin: 16px;
}
&.desc {
font-size: 14px;
}
}
.disabledDescription {
font-size: 90%;
}
</style>

View File

@@ -2,8 +2,8 @@
<XColumn :menu="menu" :naked="true" :column="column" :is-stacked="isStacked" @parent-focus="$event => emit('parent-focus', $event)">
<template #header><i class="ti ti-apps" style="margin-right: 8px;"></i>{{ column.name }}</template>
<div class="wtdtxvec">
<div v-if="!(column.widgets && column.widgets.length > 0) && !edit" class="intro">{{ i18n.ts._deck.widgetsIntroduction }}</div>
<div :class="$style.root">
<div v-if="!(column.widgets && column.widgets.length > 0) && !edit" :class="$style.intro">{{ i18n.ts._deck.widgetsIntroduction }}</div>
<XWidgets :edit="edit" :widgets="column.widgets ?? []" @add-widget="addWidget" @remove-widget="removeWidget" @update-widget="updateWidget" @update-widgets="updateWidgets" @exit="edit = false"/>
</div>
</XColumn>
@@ -54,16 +54,16 @@ const menu = [{
}];
</script>
<style lang="scss" scoped>
.wtdtxvec {
<style lang="scss" module>
.root {
--margin: 8px;
--panelBorder: none;
padding: 0 var(--margin);
}
> .intro {
padding: 16px;
text-align: center;
}
.intro {
padding: 16px;
text-align: center;
}
</style>

View File

@@ -1,3 +1,4 @@
import path from 'path';
import pluginVue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
@@ -62,7 +63,8 @@ export default defineConfig(({ command, mode }) => {
if (process.env.NODE_ENV === 'production') {
return 'x' + toBase62(hash(`${filename} ${name}`)).substring(0, 4);
} else {
return 'x' + toBase62(hash(`${filename} ${name}`)).substring(0, 4) + '-' + name;
//return 'x' + toBase62(hash(`${filename} ${name}`)).substring(0, 4) + '-' + name;
return (path.relative(__dirname, filename.split('?')[0]) + '-' + name).replace(/[\\\/\.\?&=]/g, '-').replace(/(src-|vue-)/g, '');
}
},
},