Merge branch 'develop' of https://codeberg.org/thatonecalculator/calckey into supakaity/user-rss-atom-json

This commit is contained in:
Kaity A 2022-12-01 15:54:23 +00:00
commit 674bba5911
27 changed files with 182 additions and 120 deletions

View file

@ -87,7 +87,9 @@
- AVIF support - AVIF support
- Page drafts - Page drafts
- Patron list - Patron list
- Animations respect reduced motion
- Obliteration of Ai-chan - 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) - [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) - [Tapping avatar in mobile opens account modal](https://github.com/misskey-dev/misskey/pull/9056)
- [OAuth bearer token authentication](https://github.com/misskey-dev/misskey/pull/9021) - [OAuth bearer token authentication](https://github.com/misskey-dev/misskey/pull/9021)

View file

@ -20,6 +20,7 @@
- Improved UI/UX (especially on mobile) - Improved UI/UX (especially on mobile)
- Improved notifications - Improved notifications
- Improved instance security - Improved instance security
- Improved accessibility
- Recommended Instances timeline - Recommended Instances timeline
- OCR image captioning - OCR image captioning
- New and improved Groups - New and improved Groups
@ -48,13 +49,27 @@ This guide will work for both **starting from scratch** and **migrating from Mis
## 📦 Dependencies ## 📦 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.1.0 recommended)
- Install with [nvm](https://github.com/nvm-sh/nvm)
- 🐘 At least [PostgreSQL](https://www.postgresql.org/) v12 - 🐘 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)
- 🛰️ (Optional, for non-Docker) [pm2](https://pm2.io/) ### 😗 Optional dependencies
- 📗 [FFmpeg](https://ffmpeg.org/) for video transcoding
- 🔍 [ElasticSearch](https://www.elastic.co/elasticsearch/) for full-text search
- OpenSearch/Sonic are not supported as of right now
- 🥡 Management (choose one of the following)
- 🛰️ [pm2](https://pm2.io/)
- 🐳 [Docker](https://docker.com)
- 📐 Service manager (systemd, openrc, etc)
### 🏗️ Build dependencies
- 🦬 C/C++ compiler & build tools
- `build-essential` on Debian/Ubuntu Linux
- `base-devel` on Arch Linux
- 🐍 [Python 3](https://www.python.org/)
## 👀 Get folder ready ## 👀 Get folder ready
@ -71,10 +86,19 @@ cd calckey/
corepack enable corepack enable
``` ```
## 🐘 Create database
Assuming you set up PostgreSQL correctly, all you have to run is:
```sh
psql postgres -c "create database calckey with encoding = 'UTF8';"
```
## 💅 Customize ## 💅 Customize
- To add custom CSS for all users, edit `./custom/instance.css`. - To add custom CSS for all users, edit `./custom/assets/instance.css`.
- To add static assets (such as images for the splash screen), place them in the `./custom/` directory. They'll then be avaliable on `https://yourinstance.tld/static-assets/filename.ext`. - To add static assets (such as images for the splash screen), place them in the `./custom/assets/` directory. They'll then be avaliable on `https://yourinstance.tld/static-assets/filename.ext`.
- To add custom locales, place them in the `./custom/locales/` directory. If you name your custom locale the same as an existing locale, it will overwrite it. If you give it a unique name, it will be added to the list. Also make sure that the first part of the filename matches the locale you're basing it on. (Example: `en-FOO.yml`)
- To update custom assets without rebuilding, just run `yarn run gulp`. - To update custom assets without rebuilding, just run `yarn run gulp`.
## 🧑‍🔬 Configuring a new instance ## 🧑‍🔬 Configuring a new instance
@ -103,7 +127,7 @@ cp -r ../misskey/files . # if you don't use object storage
## 🚀 Build and launch! ## 🚀 Build and launch!
### 🐢 NodeJS ### 🐢 NodeJS + pm2
#### `git pull` and run these steps to update Calckey in the future! #### `git pull` and run these steps to update Calckey in the future!

0
custom/locales/.gitkeep Normal file
View file

View file

@ -16,7 +16,7 @@ gulp.task('copy:backend:views', () =>
); );
gulp.task('copy:backend:custom', () => gulp.task('copy:backend:custom', () =>
gulp.src('./custom/*').pipe(gulp.dest('./packages/backend/assets/')) gulp.src('./custom/assets/*').pipe(gulp.dest('./packages/backend/assets/'))
); );
gulp.task('copy:client:fonts', () => gulp.task('copy:client:fonts', () =>

View file

@ -4,6 +4,8 @@
const fs = require('fs'); const fs = require('fs');
const yaml = require('js-yaml'); const yaml = require('js-yaml');
let languages = []
let languages_custom = []
const merge = (...args) => args.reduce((a, c) => ({ const merge = (...args) => args.reduce((a, c) => ({
...a, ...a,
@ -13,33 +15,20 @@ const merge = (...args) => args.reduce((a, c) => ({
.reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {}) .reduce((a, [k, v]) => (a[k] = merge(v, c[k]), a), {})
}), {}); }), {});
const languages = [
'ar-SA', fs.readdirSync(__dirname).forEach((file) => {
'cs-CZ', if (file.includes('.yml')){
'da-DK', file = file.slice(0, file.indexOf('.'))
'de-DE', languages.push(file);
'en-US', }
'es-ES', })
'fr-FR',
'id-ID', fs.readdirSync(__dirname + '/../custom/locales').forEach((file) => {
'it-IT', if (file.includes('.yml')){
'ja-JP', file = file.slice(0, file.indexOf('.'))
'ja-KS', languages_custom.push(file);
'kab-KAB', }
'kn-IN', })
'ko-KR',
'nl-NL',
'no-NO',
'pl-PL',
'pt-PT',
'ru-RU',
'sk-SK',
'ug-CN',
'uk-UA',
'vi-VN',
'zh-CN',
'zh-TW',
];
const primaries = { const primaries = {
'en': 'US', 'en': 'US',
@ -51,6 +40,8 @@ const primaries = {
const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), ''); const clean = (text) => text.replace(new RegExp(String.fromCodePoint(0x08), 'g'), '');
const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8'))) || {}, a), {}); const locales = languages.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/${c}.yml`, 'utf-8'))) || {}, a), {});
const locales_custom = languages_custom.reduce((a, c) => (a[c] = yaml.load(clean(fs.readFileSync(`${__dirname}/../custom/locales/${c}.yml`, 'utf-8'))) || {}, a), {});
Object.assign(locales, locales_custom)
module.exports = Object.entries(locales) module.exports = Object.entries(locales)
.reduce((a, [k ,v]) => (a[k] = (() => { .reduce((a, [k ,v]) => (a[k] = (() => {

View file

@ -1,6 +1,6 @@
{ {
"name": "calckey", "name": "calckey",
"version": "12.119.0-calc.17.6", "version": "12.119.0-calc.18-rc.7",
"codename": "aqua", "codename": "aqua",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -1,14 +1,14 @@
import { In, Repository } from 'typeorm'; import { In, Repository } from 'typeorm';
import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index.js';
import { Notification } from '@/models/entities/notification.js'; import { Notification } from '@/models/entities/notification.js';
import { awaitAll } from '@/prelude/await-all.js'; import { awaitAll } from '@/prelude/await-all.js';
import { Packed } from '@/misc/schema.js'; import type { Packed } from '@/misc/schema.js';
import { Note } from '@/models/entities/note.js'; import type { Note } from '@/models/entities/note.js';
import { NoteReaction } from '@/models/entities/note-reaction.js'; import type { NoteReaction } from '@/models/entities/note-reaction.js';
import { User } from '@/models/entities/user.js'; import type { User } from '@/models/entities/user.js';
import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis.js'; import { aggregateNoteEmojis, prefetchEmojis } from '@/misc/populate-emojis.js';
import { notificationTypes } from '@/types.js'; import { notificationTypes } from '@/types.js';
import { db } from '@/db/postgre.js'; import { db } from '@/db/postgre.js';
import { Users, Notes, UserGroupInvitations, AccessTokens, NoteReactions } from '../index.js';
export const NotificationRepository = db.getRepository(Notification).extend({ export const NotificationRepository = db.getRepository(Notification).extend({
async pack( async pack(
@ -17,7 +17,7 @@ export const NotificationRepository = db.getRepository(Notification).extend({
_hintForEachNotes_?: { _hintForEachNotes_?: {
myReactions: Map<Note['id'], NoteReaction | null>; myReactions: Map<Note['id'], NoteReaction | null>;
}; };
} },
): Promise<Packed<'Notification'>> { ): Promise<Packed<'Notification'>> {
const notification = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src }); const notification = typeof src === 'object' ? src : await this.findOneByOrFail({ id: src });
const token = notification.appAccessTokenId ? await AccessTokens.findOneByOrFail({ id: notification.appAccessTokenId }) : null; const token = notification.appAccessTokenId ? await AccessTokens.findOneByOrFail({ id: notification.appAccessTokenId }) : null;
@ -86,7 +86,7 @@ export const NotificationRepository = db.getRepository(Notification).extend({
async packMany( async packMany(
notifications: Notification[], notifications: Notification[],
meId: User['id'] meId: User['id'],
) { ) {
if (notifications.length === 0) return []; if (notifications.length === 0) return [];
@ -106,10 +106,15 @@ export const NotificationRepository = db.getRepository(Notification).extend({
await prefetchEmojis(aggregateNoteEmojis(notes)); await prefetchEmojis(aggregateNoteEmojis(notes));
return await Promise.all(notifications.map(x => this.pack(x, { const results = await Promise.all(notifications
_hintForEachNotes_: { .map(x =>
myReactions: myReactionsMap, this.pack(x, {
}, _hintForEachNotes_: {
}))); myReactions: myReactionsMap,
},
}).catch(e => null),
),
);
return results.filter(x => x != null);
}, },
}); });

View file

@ -26,7 +26,7 @@ export default async (actor: CacheableRemoteUser, activity: IUpdate): Promise<st
await updatePerson(actor.uri!, resolver, object); await updatePerson(actor.uri!, resolver, object);
return `ok: Person updated`; return `ok: Person updated`;
} else if (getApType(object) === 'Question') { } else if (getApType(object) === 'Question') {
await updateQuestion(object).catch(e => console.log(e)); await updateQuestion(object, resolver).catch(e => console.log(e));
return `ok: Question updated`; return `ok: Question updated`;
} else { } else {
return `skip: Unknown type: ${getApType(object)}`; return `skip: Unknown type: ${getApType(object)}`;

View file

@ -271,7 +271,7 @@ export async function createPerson(uri: string, resolver?: Resolver): Promise<Us
}); });
//#endregion //#endregion
await updateFeatured(user!.id).catch(err => logger.error(err)); await updateFeatured(user!.id, resolver).catch(err => logger.error(err));
return user!; return user!;
} }
@ -384,7 +384,7 @@ export async function updatePerson(uri: string, resolver?: Resolver | null, hint
followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined), followerSharedInbox: person.sharedInbox || (person.endpoints ? person.endpoints.sharedInbox : undefined),
}); });
await updateFeatured(exist.id).catch(err => logger.error(err)); await updateFeatured(exist.id, resolver).catch(err => logger.error(err));
} }
/** /**
@ -462,14 +462,14 @@ export function analyzeAttachments(attachments: IObject | IObject[] | undefined)
return { fields, services }; return { fields, services };
} }
export async function updateFeatured(userId: User['id']) { export async function updateFeatured(userId: User['id'], resolver?: Resolver) {
const user = await Users.findOneByOrFail({ id: userId }); const user = await Users.findOneByOrFail({ id: userId });
if (!Users.isRemoteUser(user)) return; if (!Users.isRemoteUser(user)) return;
if (!user.featured) return; if (!user.featured) return;
logger.info(`Updating the featured: ${user.uri}`); logger.info(`Updating the featured: ${user.uri}`);
const resolver = new Resolver(); if (resolver == null) resolver = new Resolver();
// Resolve to (Ordered)Collection Object // Resolve to (Ordered)Collection Object
const collection = await resolver.resolveCollection(user.featured); const collection = await resolver.resolveCollection(user.featured);

View file

@ -40,7 +40,7 @@ export async function extractPollFromQuestion(source: string | IObject, resolver
* @param uri URI of AP Question object * @param uri URI of AP Question object
* @returns true if updated * @returns true if updated
*/ */
export async function updateQuestion(value: any) { export async function updateQuestion(value: any, resolver?: Resolver) {
const uri = typeof value === 'string' ? value : value.id; const uri = typeof value === 'string' ? value : value.id;
// URIがこのサーバーを指しているならスキップ // URIがこのサーバーを指しているならスキップ
@ -55,7 +55,7 @@ export async function updateQuestion(value: any) {
//#endregion //#endregion
// resolve new Question object // resolve new Question object
const resolver = new Resolver(); if (resolver == null) resolver = new Resolver();
const question = await resolver.resolve(value) as IQuestion; const question = await resolver.resolve(value) as IQuestion;
apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`); apLogger.debug(`fetched question: ${JSON.stringify(question, null, 2)}`);

View file

@ -19,9 +19,11 @@ import renderFollow from '@/remote/activitypub/renderer/follow.js';
export default class Resolver { export default class Resolver {
private history: Set<string>; private history: Set<string>;
private user?: ILocalUser; private user?: ILocalUser;
private recursionLimit?: number;
constructor() { constructor(recursionLimit = 100) {
this.history = new Set(); this.history = new Set();
this.recursionLimit = recursionLimit;
} }
public getHistory(): string[] { public getHistory(): string[] {
@ -59,7 +61,9 @@ export default class Resolver {
if (this.history.has(value)) { if (this.history.has(value)) {
throw new Error('cannot resolve already resolved one'); throw new Error('cannot resolve already resolved one');
} }
if (this.recursionLimit && this.history.size > this.recursionLimit) {
throw new Error('hit recursion limit');
}
this.history.add(value); this.history.add(value);
const host = extractDbHost(value); const host = extractDbHost(value);

View file

@ -42,7 +42,7 @@ html {
width: 28px; width: 28px;
height: 28px; height: 28px;
transform: translateY(110px); transform: translateY(110px);
display: none !important; display: none;
color: var(--accent); color: var(--accent);
} }
#splashSpinner > .spinner { #splashSpinner > .spinner {
@ -101,6 +101,16 @@ html {
} }
} }
@media(prefers-reduced-motion) {
#splashSpinner {
display: block;
}
#splashIcon {
animation: none;
}
}
#splashText { #splashText {
position: absolute; position: absolute;
top: 0; top: 0;

View file

@ -64,6 +64,7 @@ const bg = {
font-size: 0.9em; font-size: 0.9em;
vertical-align: top; vertical-align: top;
font-weight: bold; font-weight: bold;
text-overflow: clip;
} }
} }
</style> </style>

View file

@ -71,21 +71,21 @@
</div> </div>
<footer class="footer"> <footer class="footer">
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/> <XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
<button class="button _button" @click="reply()"> <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-bold ph-lg"></i></template> <template v-if="appearNote.reply"><i class="ph-arrow-u-up-left-bold ph-lg"></i></template>
<template v-else><i class="ph-arrow-bend-up-left-bold ph-lg"></i></template> <template v-else><i class="ph-arrow-bend-up-left-bold ph-lg"></i></template>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p> <p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
</button> </button>
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/> <XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/> <XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()"> <button v-if="appearNote.myReaction == null" ref="reactButton" v-tooltip.noDelay.bottom="i18n.ts.reaction" class="button _button" @click="react()">
<i class="ph-smiley-bold ph-lg"></i> <i class="ph-smiley-bold ph-lg"></i>
</button> </button>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<i class="ph-minus-bold ph-lg"></i> <i class="ph-minus-bold ph-lg"></i>
</button> </button>
<XQuoteButton class="button" :note="appearNote"/> <XQuoteButton class="button" :note="appearNote"/>
<button ref="menuButton" class="button _button" @click="menu()"> <button ref="menuButton" v-tooltip.noDelay.bottom="i18n.ts.more" class="button _button" @click="menu()">
<i class="ph-dots-three-outline-bold ph-lg"></i> <i class="ph-dots-three-outline-bold ph-lg"></i>
</button> </button>
</footer> </footer>
@ -427,7 +427,7 @@ function readPromo() {
display: flex; display: flex;
padding: 28px 32px 18px; padding: 28px 32px 18px;
cursor: pointer; cursor: pointer;
> .avatar { > .avatar {
flex-shrink: 0; flex-shrink: 0;
display: block; display: block;

View file

@ -81,21 +81,21 @@
</MkA> </MkA>
</div> </div>
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/> <XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
<button class="button _button" @click="reply()"> <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-bold ph-lg"></i></template> <template v-if="appearNote.reply"><i class="ph-arrow-u-up-left-bold ph-lg"></i></template>
<template v-else><i class="ph-arrow-bend-up-left-bold ph-lg"></i></template> <template v-else><i class="ph-arrow-bend-up-left-bold ph-lg"></i></template>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p> <p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
</button> </button>
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/> <XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/> <XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
<button v-if="appearNote.myReaction == null" ref="reactButton" class="button _button" @click="react()"> <button v-if="appearNote.myReaction == null" ref="reactButton" v-tooltip.noDelay.bottom="i18n.ts.reaction" class="button _button" @click="react()">
<i class="ph-smiley-bold ph-lg"></i> <i class="ph-smiley-bold ph-lg"></i>
</button> </button>
<button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)"> <button v-if="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<i class="ph-minus-bold ph-lg"></i> <i class="ph-minus-bold ph-lg"></i>
</button> </button>
<XQuoteButton class="button" :note="appearNote"/> <XQuoteButton class="button" :note="appearNote"/>
<button ref="menuButton" class="button _button" @click="menu()"> <button ref="menuButton" v-tooltip.noDelay.bottom="i18n.ts.more" class="button _button" @click="menu()">
<i class="ph-dots-three-outline-bold ph-lg"></i> <i class="ph-dots-three-outline-bold ph-lg"></i>
</button> </button>
</footer> </footer>
@ -117,7 +117,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue'; import { computed, inject, onMounted, onUnmounted, reactive, ref } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSub from '@/components/MkNoteSub.vue';
import XNoteSimple from '@/components/MkNoteSimple.vue'; import XNoteSimple from '@/components/MkNoteSimple.vue';
import XReactionsViewer from '@/components/MkReactionsViewer.vue'; import XReactionsViewer from '@/components/MkReactionsViewer.vue';

View file

@ -1,6 +1,7 @@
<template> <template>
<button <button
v-if="canRenote && $store.state.seperateRenoteQuote" v-if="canRenote && $store.state.seperateRenoteQuote"
v-tooltip.noDelay.bottom="i18n.ts.quote"
class="eddddedb _button" class="eddddedb _button"
@click="quote()" @click="quote()"
> >
@ -14,6 +15,7 @@ import type { Note } from 'misskey-js/built/entities';
import { pleaseLogin } from '@/scripts/please-login'; import { pleaseLogin } from '@/scripts/please-login';
import * as os from '@/os'; import * as os from '@/os';
import { $i } from '@/account'; import { $i } from '@/account';
import { i18n } from '@/i18n';
const props = defineProps<{ const props = defineProps<{
note: Note; note: Note;

View file

@ -2,6 +2,7 @@
<button <button
v-if="canRenote" v-if="canRenote"
ref="buttonRef" ref="buttonRef"
v-tooltip.noDelay.bottom="i18n.ts.renote"
class="eddddedb _button canRenote" class="eddddedb _button canRenote"
@click="renote(false, $event)" @click="renote(false, $event)"
> >
@ -15,7 +16,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import * as misskey from 'misskey-js'; import type * as misskey from 'misskey-js';
import Ripple from '@/components/MkRipple.vue'; import Ripple from '@/components/MkRipple.vue';
import XDetails from '@/components/MkUsersTooltip.vue'; import XDetails from '@/components/MkUsersTooltip.vue';
import { pleaseLogin } from '@/scripts/please-login'; import { pleaseLogin } from '@/scripts/please-login';
@ -23,7 +24,7 @@ import * as os from '@/os';
import { $i } from '@/account'; import { $i } from '@/account';
import { useTooltip } from '@/scripts/use-tooltip'; import { useTooltip } from '@/scripts/use-tooltip';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { defaultStore } from "@/store"; import { defaultStore } from '@/store';
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;

View file

@ -65,6 +65,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue'; import { onMounted, onUnmounted, ref } from 'vue';
import { reducedMotion } from '@/scripts/reduced-motion';
const particles = ref([]); const particles = ref([]);
const el = ref<HTMLElement>(); const el = ref<HTMLElement>();
@ -75,34 +76,36 @@ let stop = false;
let ro: ResizeObserver | undefined; let ro: ResizeObserver | undefined;
onMounted(() => { onMounted(() => {
ro = new ResizeObserver((entries, observer) => { if (!reducedMotion()) {
width.value = el.value?.offsetWidth + 64; ro = new ResizeObserver((entries, observer) => {
height.value = el.value?.offsetHeight + 64; width.value = el.value?.offsetWidth + 64;
}); height.value = el.value?.offsetHeight + 64;
ro.observe(el.value); });
const add = () => { ro.observe(el.value);
if (stop) return; const add = () => {
const x = (Math.random() * (width.value - 64)); if (stop) return;
const y = (Math.random() * (height.value - 64)); const x = (Math.random() * (width.value - 64));
const sizeFactor = Math.random(); const y = (Math.random() * (height.value - 64));
const particle = { const sizeFactor = Math.random();
id: Math.random().toString(), const particle = {
x, id: Math.random().toString(),
y, x,
size: 0.2 + ((sizeFactor / 10) * 3), y,
dur: 1000 + (sizeFactor * 1000), size: 0.2 + ((sizeFactor / 10) * 3),
color: colors[Math.floor(Math.random() * colors.length)], dur: 1000 + (sizeFactor * 1000),
}; color: colors[Math.floor(Math.random() * colors.length)],
particles.value.push(particle); };
window.setTimeout(() => { particles.value.push(particle);
particles.value = particles.value.filter(x => x.id !== particle.id); window.setTimeout(() => {
}, particle.dur - 100); particles.value = particles.value.filter(x => x.id !== particle.id);
}, particle.dur - 100);
window.setTimeout(() => { window.setTimeout(() => {
add(); add();
}, 500 + (Math.random() * 500)); }, 500 + (Math.random() * 500));
}; };
add(); add();
}
}); });
onUnmounted(() => { onUnmounted(() => {

View file

@ -1,5 +1,5 @@
<template> <template>
<button class="skdfgljsdkf _button" @click="star($event)"> <button v-tooltip.noDelay.bottom="i18n.ts._gallery.like" class="skdfgljsdkf _button" @click="star($event)">
<i class="ph-star-bold ph-lg"></i> <i class="ph-star-bold ph-lg"></i>
</button> </button>
</template> </template>
@ -9,6 +9,7 @@ import type { Note } from 'misskey-js/built/entities';
import Ripple from '@/components/MkRipple.vue'; import Ripple from '@/components/MkRipple.vue';
import { pleaseLogin } from '@/scripts/please-login'; import { pleaseLogin } from '@/scripts/please-login';
import * as os from '@/os'; import * as os from '@/os';
import { i18n } from '@/i18n';
const props = defineProps<{ const props = defineProps<{
note: Note; note: Note;

View file

@ -1,5 +1,6 @@
import { VNode, defineComponent, h } from 'vue'; import { defineComponent, h } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import type { VNode } from 'vue';
import MkUrl from '@/components/global/MkUrl.vue'; import MkUrl from '@/components/global/MkUrl.vue';
import MkLink from '@/components/MkLink.vue'; import MkLink from '@/components/MkLink.vue';
import MkMention from '@/components/MkMention.vue'; import MkMention from '@/components/MkMention.vue';
@ -12,6 +13,7 @@ import MkSparkle from '@/components/MkSparkle.vue';
import MkA from '@/components/global/MkA.vue'; import MkA from '@/components/global/MkA.vue';
import { host } from '@/config'; import { host } from '@/config';
import { MFM_TAGS } from '@/scripts/mfm-tags'; import { MFM_TAGS } from '@/scripts/mfm-tags';
import { reducedMotion } from '@/scripts/reduced-motion';
export default defineComponent({ export default defineComponent({
props: { props: {
@ -97,17 +99,17 @@ export default defineComponent({
} }
case 'jelly': { case 'jelly': {
const speed = validTime(token.props.args.speed) || '1s'; const speed = validTime(token.props.args.speed) || '1s';
style = (this.$store.state.animatedMfm ? `animation: mfm-rubberBand ${speed} linear infinite both;` : ''); style = (this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
break; break;
} }
case 'twitch': { case 'twitch': {
const speed = validTime(token.props.args.speed) || '0.5s'; const speed = validTime(token.props.args.speed) || '0.5s';
style = this.$store.state.animatedMfm ? `animation: mfm-twitch ${speed} ease infinite;` : ''; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-twitch ${speed} ease infinite;` : '';
break; break;
} }
case 'shake': { case 'shake': {
const speed = validTime(token.props.args.speed) || '0.5s'; const speed = validTime(token.props.args.speed) || '0.5s';
style = this.$store.state.animatedMfm ? `animation: mfm-shake ${speed} ease infinite;` : ''; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-shake ${speed} ease infinite;` : '';
break; break;
} }
case 'spin': { case 'spin': {
@ -120,19 +122,30 @@ export default defineComponent({
token.props.args.y ? 'mfm-spinY' : token.props.args.y ? 'mfm-spinY' :
'mfm-spin'; 'mfm-spin';
const speed = validTime(token.props.args.speed) || '1.5s'; const speed = validTime(token.props.args.speed) || '1.5s';
style = this.$store.state.animatedMfm ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : ''; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
break; break;
} }
case 'jump': { case 'jump': {
const speed = validTime(token.props.args.speed) || '0.75s'; const speed = validTime(token.props.args.speed) || '0.75s';
style = this.$store.state.animatedMfm ? `animation: mfm-jump ${speed} linear infinite;` : ''; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-jump ${speed} linear infinite;` : '';
break; break;
} }
case 'bounce': { case 'bounce': {
const speed = validTime(token.props.args.speed) || '0.75s'; const speed = validTime(token.props.args.speed) || '0.75s';
style = this.$store.state.animatedMfm ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : ''; style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
break; break;
} }
case 'rainbow': {
const speed = validTime(token.props.args.speed) || '1s';
style = this.$store.state.animatedMfm && !reducedMotion() ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
break;
}
case 'sparkle': {
if (!this.$store.state.animatedMfm && !reducedMotion()) {
return genEl(token.children);
}
return h(MkSparkle, {}, genEl(token.children));
}
case 'flip': { case 'flip': {
const transform = const transform =
(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' : (token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
@ -173,17 +186,6 @@ export default defineComponent({
class: '_mfm_blur_', class: '_mfm_blur_',
}, genEl(token.children)); }, genEl(token.children));
} }
case 'rainbow': {
const speed = validTime(token.props.args.speed) || '1s';
style = this.$store.state.animatedMfm ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
break;
}
case 'sparkle': {
if (!this.$store.state.animatedMfm) {
return genEl(token.children);
}
return h(MkSparkle, {}, genEl(token.children));
}
case 'rotate': { case 'rotate': {
const rotate = const rotate =
token.props.args.x ? 'perspective(128px) rotateX' : token.props.args.x ? 'perspective(128px) rotateX' :

View file

@ -3,7 +3,7 @@
*/ */
import '@/style.scss'; import '@/style.scss';
import '@/icons.css'; import '@/icons.scss';
//#region account indexedDB migration //#region account indexedDB migration
import { set } from '@/scripts/idb-proxy'; import { set } from '@/scripts/idb-proxy';
@ -296,7 +296,7 @@ import { getAccountFromId } from '@/scripts/get-account-from-id';
}, { immediate: true }); }, { immediate: true });
watch(defaultStore.reactiveState.useBlurEffect, v => { watch(defaultStore.reactiveState.useBlurEffect, v => {
if (v) { if (v && deviceKind !== 'smartphone') {
document.documentElement.style.removeProperty('--blur'); document.documentElement.style.removeProperty('--blur');
} else { } else {
document.documentElement.style.setProperty('--blur', 'none'); document.documentElement.style.setProperty('--blur', 'none');

View file

@ -90,7 +90,6 @@ function del(): void {
min-height: 38px; min-height: 38px;
border-radius: 16px; border-radius: 16px;
max-width: 100%; max-width: 100%;
margin-left: 4%;
& + * { & + * {
clear: both; clear: both;
@ -215,8 +214,6 @@ function del(): void {
> .balloon { > .balloon {
$color: var(--X4); $color: var(--X4);
margin-right: 4%;
margin-left: 0%;
background: $color; background: $color;
&.noText { &.noText {

View file

@ -0,0 +1,3 @@
export function reducedMotion(): boolean {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}

View file

@ -568,6 +568,22 @@ hr {
} }
} }
@media(prefers-reduced-motion) {
@keyframes tada {
from {
transform: scale3d(1, 1, 1);
}
50% {
transform: scale3d(1.1, 1.1, 1.1);
}
to {
transform: scale3d(1, 1, 1);
}
}
}
._anime_bounce { ._anime_bounce {
will-change: transform; will-change: transform;
animation: bounce ease 0.7s; animation: bounce ease 0.7s;

View file

@ -11,7 +11,7 @@
<XStreamIndicator/> <XStreamIndicator/>
<div v-if="pendingApiRequestsCount > 0" id="wait"></div> <!-- <div v-if="pendingApiRequestsCount > 0" id="wait"></div> -->
<div v-if="dev" id="devTicker"><span>DEV BUILD</span></div> <div v-if="dev" id="devTicker"><span>DEV BUILD</span></div>
</template> </template>