Merge branch 'develop' into account_migration

This commit is contained in:
ThatOneCalculator 2022-12-06 15:34:50 -08:00
commit 7e3f8e8166
15 changed files with 241 additions and 91 deletions

View file

@ -3,6 +3,9 @@
## Planned
- MFM button
- Sonic search support
- Use Cassandra for storing notes
- Federate with note edits
- Classic mode make instance icon bring up new context menu
- Exclude self from antenna
- Backfill remote users
@ -19,17 +22,14 @@
## Work in progress
- Account migration
- Link verification
- Better Messaging UI
- Better API Documentation
- Remote follow button
- Admin custom CSS
- Add back time machine (jump to date)
- Improve accesibility score
<details><summary>Current Misskey score is 57/100</summary>
![accesibility score](https://pool.jortage.com/voringme/misskey/8ff18aae-4dc6-4b08-9e05-a4c9d051a9e3.png)
</details>
- Improve accesibility
## Implemented
@ -88,8 +88,9 @@
- Page drafts
- Patron list
- Animations respect reduced motion
- Obliteration of Ai-chan
- Undo renote button inside original note
- Custom locales
- Obliteration of Ai-chan
- MissV: [fix Misskey Forkbomb](https://code.vtopia.live/Vtopia/MissV/commit/40b23c070bd4adbb3188c73546c6c625138fb3c1)
- [Make showing ads optional](https://github.com/misskey-dev/misskey/pull/8996)
- [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056)
@ -131,3 +132,4 @@
- 4c5aa9e53887cca5561fcec6ab0754e018f589a5: server: allow to like own pages
- 923c93da1228458dd65be47483c198a1a9191bcf: use await for notes.countBy
- ca90cedba0a0704b503c2778694230f5a7dfbace: server: reduce dead instance detection to 7 days
- e9ab42c10afb4e27516c2d2b5e3e06630efe9edd: Alt text in image viewer

View file

@ -49,10 +49,10 @@ This guide will work for both **starting from scratch** and **migrating from Mis
## 📦 Dependencies
- 🐢 At least [NodeJS](https://nodejs.org/en/) v18.12.1 (v19.1.0 recommended)
- 🐢 At least [NodeJS](https://nodejs.org/en/) v18.12.1 (v19 recommended)
- Install with [nvm](https://github.com/nvm-sh/nvm)
- 🐘 At least [PostgreSQL](https://www.postgresql.org/) v12
- 🍱 At least [Redis](https://redis.io/) v6 (v7 recommended)
- 🍱 At least [Redis](https://redis.io/) v6 (v7 recommended) or [Titan](https://github.com/distributedio/titan)
### 😗 Optional dependencies

View file

@ -33,7 +33,6 @@ export async function skippedInstances(hosts: Array<Instace['host']>): Array<Ins
})
.andWhere(new Brackets(qb => { qb
.where('instance.isSuspended')
.orWhere('instance.lastCommunicatedAt < :deadTime', { deadTime });
}))
.select('host')
.getRawMany()

View file

@ -306,6 +306,7 @@ import * as ep___users_groups_transfer from './endpoints/users/groups/transfer.j
import * as ep___users_groups_update from './endpoints/users/groups/update.js';
import * as ep___users_lists_create from './endpoints/users/lists/create.js';
import * as ep___users_lists_delete from './endpoints/users/lists/delete.js';
import * as ep___users_lists_delete_all from './endpoints/users/lists/delete-all.js';
import * as ep___users_lists_list from './endpoints/users/lists/list.js';
import * as ep___users_lists_pull from './endpoints/users/lists/pull.js';
import * as ep___users_lists_push from './endpoints/users/lists/push.js';
@ -631,6 +632,7 @@ const eps = [
['users/groups/update', ep___users_groups_update],
['users/lists/create', ep___users_lists_create],
['users/lists/delete', ep___users_lists_delete],
['users/lists/delete-all', ep___users_lists_delete_all],
['users/lists/list', ep___users_lists_list],
['users/lists/pull', ep___users_lists_pull],
['users/lists/push', ep___users_lists_push],

View file

@ -0,0 +1,36 @@
import { UserLists } from '@/models/index.js';
import define from '../../../define.js';
import { ApiError } from '../../../error.js';
export const meta = {
tags: ['lists'],
requireCredential: true,
kind: 'write:account',
description: 'Delete all lists of users.',
errors: {
noSuchList: {
message: 'No such list.',
code: 'NO_SUCH_LIST',
id: '78436795-db79-42f5-b1e2-55ea2cf19166',
},
},
} as const;
export const paramDef = {
type: 'object',
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, user) => {
while (await UserLists.findOneBy({ userId: user.id }) != null) {
const userList = await UserLists.findOneBy({ userId: user.id });
if (userList == null) {
throw new ApiError(meta.errors.noSuchList);
}
await UserLists.delete(userList.id);
}
});

View file

@ -8,7 +8,8 @@
<template v-else><i class="ph-caret-down-bold ph-lg"></i></template>
</button>
</header>
<transition :name="$store.state.animation ? 'folder-toggle' : ''"
<transition
:name="$store.state.animation ? 'folder-toggle' : ''"
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
@ -27,17 +28,18 @@ import tinycolor from 'tinycolor2';
const localStoragePrefix = 'ui:folder:';
// eslint-disable-next-line import/no-default-export
export default defineComponent({
props: {
expanded: {
type: Boolean,
required: false,
default: true
default: true,
},
persistKey: {
type: String,
required: false,
default: null
default: null,
},
},
data() {
@ -51,7 +53,7 @@ export default defineComponent({
if (this.persistKey) {
localStorage.setItem(localStoragePrefix + this.persistKey, this.showBody ? 't' : 'f');
}
}
},
},
mounted() {
function getParentBg(el: Element | null): string {
@ -91,7 +93,7 @@ export default defineComponent({
afterLeave(el) {
el.style.height = null;
},
}
},
});
</script>

View file

@ -46,7 +46,7 @@ onMounted(() => {
src: media.url,
w: media.properties.width,
h: media.properties.height,
alt: media.name,
alt: media.comment,
};
if (media.properties.orientation != null && media.properties.orientation >= 5) {
[item.w, item.h] = [item.h, item.w];
@ -89,9 +89,38 @@ onMounted(() => {
[itemData.w, itemData.h] = [itemData.h, itemData.w];
}
itemData.msrc = file.thumbnailUrl;
itemData.alt = file.comment;
itemData.thumbCropped = true;
});
lightbox.on('uiRegister', () => {
lightbox.pswp.ui.registerElement({
name: 'altText',
className: 'pwsp__alt-text-container',
appendTo: 'wrapper',
onInit: (el, pwsp) => {
let textBox = document.createElement('p');
textBox.className = 'pwsp__alt-text';
el.appendChild(textBox);
let preventProp = function(ev: Event): void {
ev.stopPropagation();
};
// Allow scrolling/text selection
el.onwheel = preventProp;
el.onclick = preventProp;
el.onpointerdown = preventProp;
el.onpointercancel = preventProp;
el.onpointermove = preventProp;
pwsp.on('change', () => {
textBox.textContent = pwsp.currSlide.data.alt?.trim();
});
},
});
});
lightbox.init();
});
@ -193,4 +222,35 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
//z-index: v-bind(pswpZIndex);
z-index: 2000000;
}
.pwsp__alt-text-container {
display: flex;
flex-direction: row;
align-items: center;
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
width: 75%;
}
.pwsp__alt-text {
color: white;
margin: 0 auto;
text-align: center;
padding: 10px;
background: rgba(0, 0, 0, 0.5);
border-radius: 5px;
max-height: 10vh;
overflow-x: clip;
overflow-y: auto;
overscroll-behavior: contain;
}
.pwsp__alt-text:empty {
display: none;
}
</style>

View file

@ -1,8 +1,7 @@
<template>
<XModalWindow
ref="dialog"
:width="370"
:height="400"
:width="400"
@close="onClose"
@closed="emit('closed')"
>

View file

@ -1,65 +1,67 @@
<template>
<form class="qlvuhzng _formRoot" autocomplete="new-password" @submit.prevent="onSubmit">
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required>
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" class="_formBlock" type="text" :spellcheck="false" required data-cy-signup-invitation-code @update:modelValue="onChangeInvitationCode">
<template #label>{{ i18n.ts.invitationCode }}</template>
<template #prefix><i class="ph-key-bold ph-lg"></i></template>
</MkInput>
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ph-question-bold"></i></div></template>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
<template #caption>
<span v-if="usernameState === 'wait'" style="color:#999"><i class="ph-circle-notch-bold ph-lg fa-pulse ph-fw ph-lg"></i> {{ i18n.ts.checking }}</span>
<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.available }}</span>
<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.unavailable }}</span>
<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.error }}</span>
<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.tooShort }}</span>
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.tooLong }}</span>
</template>
</MkInput>
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ph-question-bold"></i></div></template>
<template #prefix><i class="ph-envelope-simple-open-bold ph-lg"></i></template>
<template #caption>
<span v-if="emailState === 'wait'" style="color:#999"><i class="ph-circle-notch-bold ph-lg fa-pulse ph-fw ph-lg"></i> {{ i18n.ts.checking }}</span>
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.available }}</span>
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.used }}</span>
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.format }}</span>
<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.unavailable }}</span>
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.error }}</span>
</template>
</MkInput>
<MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
<template #label>{{ i18n.ts.password }}</template>
<template #prefix><i class="ph-lock-bold ph-lg"></i></template>
<template #caption>
<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.weakPassword }}</span>
<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.normalPassword }}</span>
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.strongPassword }}</span>
</template>
</MkInput>
<MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
<template #prefix><i class="ph-lock-bold ph-lg"></i></template>
<template #caption>
<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.passwordMatched }}</span>
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.passwordNotMatched }}</span>
</template>
</MkInput>
<MkSwitch v-if="instance.tosUrl" v-model="ToSAgreement" class="_formBlock tou">
<I18n :src="i18n.ts.agreeTo">
<template #0>
<a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.tos }}</a>
<div v-if="!instance.disableRegistration || (instance.disableRegistration && invitationState === 'entered')">
<MkInput v-model="username" class="_formBlock" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:modelValue="onChangeUsername">
<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ph-question-bold"></i></div></template>
<template #prefix>@</template>
<template #suffix>@{{ host }}</template>
<template #caption>
<span v-if="usernameState === 'wait'" style="color:#6e6a86"><i class="ph-circle-notch-bold ph-lg fa-pulse ph-fw ph-lg"></i> {{ i18n.ts.checking }}</span>
<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.available }}</span>
<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.unavailable }}</span>
<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.error }}</span>
<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.tooShort }}</span>
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.tooLong }}</span>
</template>
</I18n>
</MkSwitch>
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ i18n.ts.start }}</MkButton>
</MkInput>
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" class="_formBlock" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ph-question-bold"></i></div></template>
<template #prefix><i class="ph-envelope-simple-open-bold ph-lg"></i></template>
<template #caption>
<span v-if="emailState === 'wait'" style="color:#6e6a86"><i class="ph-circle-notch-bold ph-lg fa-pulse ph-fw ph-lg"></i> {{ i18n.ts.checking }}</span>
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.available }}</span>
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.used }}</span>
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.format }}</span>
<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.unavailable }}</span>
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.error }}</span>
</template>
</MkInput>
<MkInput v-model="password" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
<template #label>{{ i18n.ts.password }}</template>
<template #prefix><i class="ph-lock-bold ph-lg"></i></template>
<template #caption>
<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.weakPassword }}</span>
<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.normalPassword }}</span>
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.strongPassword }}</span>
</template>
</MkInput>
<MkInput v-model="retypedPassword" class="_formBlock" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
<template #prefix><i class="ph-lock-bold ph-lg"></i></template>
<template #caption>
<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ph-check-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.passwordMatched }}</span>
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ph-warning-bold ph-lg ph-fw ph-lg"></i> {{ i18n.ts.passwordNotMatched }}</span>
</template>
</MkInput>
<MkSwitch v-if="instance.tosUrl" v-model="ToSAgreement" class="_formBlock tou">
<I18n :src="i18n.ts.agreeTo">
<template #0>
<a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.tos }}</a>
</template>
</I18n>
</MkSwitch>
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="_formBlock captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="_formBlock captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
<MkButton class="_formBlock" type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ i18n.ts.start }}</MkButton>
</div>
</form>
</template>
@ -99,6 +101,7 @@ let retypedPassword: string = $ref('');
let invitationCode: string = $ref('');
let email = $ref('');
let usernameState: null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range' = $ref(null);
let invitationState: null | 'entered' = $ref(null);
let emailState: null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error' = $ref(null);
let passwordStrength: '' | 'low' | 'medium' | 'high' = $ref('');
let passwordRetypeState: null | 'match' | 'not-match' = $ref(null);
@ -115,6 +118,14 @@ const shouldDisableSubmitting = $computed((): boolean => {
passwordRetypeState === 'not-match';
});
function onChangeInvitationCode(): void {
if (invitationCode === '') {
invitationState = null;
return;
}
invitationState = 'entered';
}
function onChangeUsername(): void {
if (username === '') {
usernameState = null;

View file

@ -1,16 +1,15 @@
<template>
<XModalWindow
ref="dialog"
:width="366"
:height="500"
@close="dialog.close()"
:width="400"
@close="dialog!.close()"
@closed="$emit('closed')"
>
<template #header>{{ i18n.ts.signup }}</template>
<div class="_monolithic_">
<div class="_section">
<XSignup :auto-set="autoSet" @signup="onSignup" @signupEmailPending="onSignupEmailPending"/>
<XSignup :auto-set="autoSet" @signup="onSignup" @signup-email-pending="onSignupEmailPending"/>
</div>
</div>
</XModalWindow>
@ -37,10 +36,10 @@ const dialog = $ref<InstanceType<typeof XModalWindow>>();
function onSignup(res) {
emit('done', res);
dialog.close();
dialog?.close();
}
function onSignupEmailPending() {
dialog.close();
dialog?.close();
}
</script>

View file

@ -3,7 +3,10 @@
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="700">
<div class="qkcjvfiv">
<MkButton primary class="add" @click="create"><i class="ph-plus-bold ph-lg"></i> {{ i18n.ts.createList }}</MkButton>
<div class="buttonWrapper">
<MkButton primary class="add" @click="create"><i class="ph-plus-bold ph-lg"></i> {{ i18n.ts.createList }}</MkButton>
<MkButton @click="deleteAll"><i class="ph-trash-bold ph-lg"></i> {{ i18n.ts.deleteAll }}</MkButton>
</div>
<MkPagination v-slot="{items}" ref="pagingComponent" :pagination="pagination" class="lists _content">
<MkA v-for="list in items" :key="list.id" class="list _panel" :to="`/my/lists/${ list.id }`">
@ -41,6 +44,17 @@ async function create() {
pagingComponent.reload();
}
async function deleteAll() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.t('removeAreYouSure', { x: 'all lists' }),
});
if (canceled) return;
await os.api('users/lists/delete-all');
os.success();
}
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
@ -57,8 +71,13 @@ definePageMetadata({
<style lang="scss" scoped>
.qkcjvfiv {
> .add {
margin: 0 auto var(--margin) auto;
> .buttonWrapper {
display: grid;
justify-content: center;
> .add {
margin: 0 auto var(--margin) auto;
}
}
> .lists {

View file

@ -15,6 +15,7 @@
<h1>{{ page.title }}</h1>
</div>
<div class="menu-actions">
<button v-tooltip="i18n.ts.copyUrl" @click="copyUrl" class="menu _button"><i class="ph-link-simple-bold ph-lg"/></button>
<MkA v-tooltip="i18n.ts._pages.viewSource" :to="`/@${username}/pages/${pageName}/view-source`" class="menu _button"><i class="ph-code-bold ph-lg"/></MkA>
<template v-if="$i && $i.id === page.userId">
<MkA v-tooltip="i18n.ts._pages.editPage" class="menu _button" :to="`/pages/edit/${page.id}`"><i class="ph-pencil-bold ph-lg"/></MkA>
@ -55,7 +56,7 @@
</div> -->
</div>
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
<MkContainer :max-height="300" :foldable="true" class="other">
<MkContainer :max-height="300" :foldable="true" :expanded="false" class="other">
<template #header><i class="ph-clock-bold ph-lg"></i> {{ i18n.ts.recentPosts }}</template>
<MkPagination v-slot="{items}" :pagination="otherPostsPagination">
<MkPagePreview v-for="page in items" :key="page.id" :page="page" class="_gap"/>
@ -80,6 +81,7 @@ import MkContainer from '@/components/MkContainer.vue';
import MkPagination from '@/components/MkPagination.vue';
import MkPagePreview from '@/components/MkPagePreview.vue';
import { i18n } from '@/i18n';
import copyToClipboard from '@/scripts/copy-to-clipboard';
import { definePageMetadata } from '@/scripts/page-metadata';
import { shareAvailable } from '@/scripts/share-available';
@ -113,6 +115,11 @@ function fetchPage() {
});
}
function copyUrl() {
copyToClipboard(window.location.href);
os.success();
}
function getBgImg(): string {
if (page.eyeCatchingImage != null) {
return `url(${page.eyeCatchingImage.url})`;

View file

@ -144,22 +144,35 @@ function top(): void {
async function chooseList(ev: MouseEvent): Promise<void> {
const lists = await os.api('users/lists/list');
const items = lists.map((list) => ({
const items = [{
type: 'link' as const,
text: i18n.ts.manageLists,
icon: 'ph-faders-horizontal-bold ph-lg',
to: '/my/lists',
}].concat(lists.map((list) => ({
type: 'link' as const,
text: list.name,
icon: '',
to: `/timeline/list/${list.id}`,
}));
})));
os.popupMenu(items, ev.currentTarget ?? ev.target);
}
async function chooseAntenna(ev: MouseEvent): Promise<void> {
const antennas = await os.api('antennas/list');
const items = antennas.map((antenna) => ({
const items = [{
type: 'link' as const,
indicate: false,
text: i18n.ts.manageAntennas,
icon: 'ph-faders-horizontal-bold ph-lg',
to: '/my/antennas',
}].concat(antennas.map((antenna) => ({
type: 'link' as const,
text: antenna.name,
icon: '',
indicate: antenna.hasUnreadNote,
to: `/timeline/antenna/${antenna.id}`,
}));
})));
os.popupMenu(items, ev.currentTarget ?? ev.target);
}

View file

@ -9,7 +9,8 @@
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import XColumn from './column.vue';
import { updateColumn , Column } from './deck-store';
import { updateColumn } from './deck-store';
import type { Column } from './deck-store';
import XNotifications from '@/components/MkNotifications.vue';
import * as os from '@/os';
import { i18n } from '@/i18n';
@ -23,7 +24,7 @@ const emit = defineEmits<{
(ev: 'parent-focus', direction: 'up' | 'down' | 'left' | 'right'): void;
}>();
function func() {
function func(): void {
os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSettingWindow.vue')), {
includingTypes: props.column.includingTypes,
}, {

View file

@ -3,7 +3,7 @@ sudo docker rmi $(docker images -q)
sudo docker compose build
sudo docker tag thatonecalculator/calckey:latest thatonecalculator/calckey:$(git describe --tags --exact-match)
sudo docker images
echo "\nPress any key to continue\n"
echo "\nPress enter to continue\n"
read
sudo docker push thatonecalculator/calckey:$(git describe --tags --exact-match)
sudo docker push thatonecalculator/calckey:latest