Merge pull request 'develop' (#9177) from develop into main
Reviewed-on: https://codeberg.org/thatonecalculator/calckey/pulls/9177
This commit is contained in:
commit
3eade37f78
45 changed files with 428 additions and 242 deletions
|
@ -87,7 +87,10 @@
|
||||||
- AVIF support
|
- AVIF support
|
||||||
- Page drafts
|
- Page drafts
|
||||||
- Patron list
|
- Patron list
|
||||||
|
- Animations respect reduced motion
|
||||||
- Obliteration of Ai-chan
|
- Obliteration of Ai-chan
|
||||||
|
- Undo renote button inside original note
|
||||||
|
- 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)
|
||||||
|
|
39
README.md
39
README.md
|
@ -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!
|
||||||
|
|
||||||
|
@ -133,6 +157,7 @@ docker-compose up -d
|
||||||
|
|
||||||
- When editing the config file, please don't fill out the settings at the bottom. They're designed *only* for managed hosting, not self hosting. Those settings are much better off being set in Calckey's control panel.
|
- When editing the config file, please don't fill out the settings at the bottom. They're designed *only* for managed hosting, not self hosting. Those settings are much better off being set in Calckey's control panel.
|
||||||
- Port 3000 (used in the default config) might be already used on your server for something else. To find an open port for Calckey, run `for p in $(seq 3000 4000); do ss -tlnH | tr -s ' ' | cut -d" " -sf4 | grep -q "${p}$" || echo "${p}"; done | head -n 1`
|
- Port 3000 (used in the default config) might be already used on your server for something else. To find an open port for Calckey, run `for p in $(seq 3000 4000); do ss -tlnH | tr -s ' ' | cut -d" " -sf4 | grep -q "${p}$" || echo "${p}"; done | head -n 1`
|
||||||
|
- I'd recommend you use a S3 Bucket/CDN for Object Storage, especially if you use Docker.
|
||||||
- I'd ***strongly*** recommend against using CloudFlare, but if you do, make sure to turn code minification off.
|
- I'd ***strongly*** recommend against using CloudFlare, but if you do, make sure to turn code minification off.
|
||||||
- For push notifications, run `npx web-push generate-vapid-keys`, the put the public and private keys into Control Panel > General > ServiceWorker.
|
- For push notifications, run `npx web-push generate-vapid-keys`, the put the public and private keys into Control Panel > General > ServiceWorker.
|
||||||
- For translations, make a [DeepL](https://deepl.com) account and generate an API key, then put it into Control Panel > General > DeepL Translation.
|
- For translations, make a [DeepL](https://deepl.com) account and generate an API key, then put it into Control Panel > General > DeepL Translation.
|
||||||
|
|
0
custom/locales/.gitkeep
Normal file
0
custom/locales/.gitkeep
Normal 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', () =>
|
||||||
|
|
|
@ -32,12 +32,12 @@ uploading: "Uploading..."
|
||||||
save: "Save"
|
save: "Save"
|
||||||
users: "Users"
|
users: "Users"
|
||||||
addUser: "Add a user"
|
addUser: "Add a user"
|
||||||
favorite: "Add to favorites"
|
favorite: "Add to bookmarks"
|
||||||
favorites: "Favorites"
|
favorites: "Bookmarks"
|
||||||
unfavorite: "Remove from favorites"
|
unfavorite: "Remove from bookmarks"
|
||||||
favorited: "Added to favorites."
|
favorited: "Added to bookmarks."
|
||||||
alreadyFavorited: "Already added to favorites."
|
alreadyFavorited: "Already added to bookmarks."
|
||||||
cantFavorite: "Couldn't add to favorites."
|
cantFavorite: "Couldn't add to bookmarks."
|
||||||
pin: "Pin to profile"
|
pin: "Pin to profile"
|
||||||
unpin: "Unpin from profile"
|
unpin: "Unpin from profile"
|
||||||
copyContent: "Copy contents"
|
copyContent: "Copy contents"
|
||||||
|
@ -679,7 +679,7 @@ disableShowingAnimatedImages: "Don't play animated images"
|
||||||
verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification."
|
verificationEmailSent: "A verification email has been sent. Please follow the included link to complete verification."
|
||||||
notSet: "Not set"
|
notSet: "Not set"
|
||||||
emailVerified: "Email has been verified"
|
emailVerified: "Email has been verified"
|
||||||
noteFavoritesCount: "Number of favorite notes"
|
noteFavoritesCount: "Number of bookmarked notes"
|
||||||
pageLikesCount: "Number of liked Pages"
|
pageLikesCount: "Number of liked Pages"
|
||||||
pageLikedCount: "Number of received Page likes"
|
pageLikedCount: "Number of received Page likes"
|
||||||
contact: "Contact"
|
contact: "Contact"
|
||||||
|
@ -1227,7 +1227,7 @@ _tutorial:
|
||||||
step5_3: "The Home {icon} timeline is where you can see posts from your followers."
|
step5_3: "The Home {icon} timeline is where you can see posts from your followers."
|
||||||
step5_4: "The Local {icon} timeline is where you can see posts from everyone else on this instance."
|
step5_4: "The Local {icon} timeline is where you can see posts from everyone else on this instance."
|
||||||
step5_5: "The Recommended {icon} timeline is where you can see posts from instances the admins recommend."
|
step5_5: "The Recommended {icon} timeline is where you can see posts from instances the admins recommend."
|
||||||
step5_6: "The Social {icon} timeline is where you can see posts from friends of your followers."
|
step5_6: "The Social {icon} timeline is your home + local."
|
||||||
step5_7: "The Global {icon} timeline is where you can see posts from every other connected instance."
|
step5_7: "The Global {icon} timeline is where you can see posts from every other connected instance."
|
||||||
step6_1: "So, what is this place?"
|
step6_1: "So, what is this place?"
|
||||||
step6_2: "Well, you didn't just join Calckey. You joined a portal to the Fediverse, an interconnected network of thousands of servers, called \"instances\"."
|
step6_2: "Well, you didn't just join Calckey. You joined a portal to the Fediverse, an interconnected network of thousands of servers, called \"instances\"."
|
||||||
|
@ -1250,8 +1250,8 @@ _permissions:
|
||||||
"write:blocks": "Edit your list of blocked users"
|
"write:blocks": "Edit your list of blocked users"
|
||||||
"read:drive": "Access your Drive files and folders"
|
"read:drive": "Access your Drive files and folders"
|
||||||
"write:drive": "Edit or delete your Drive files and folders"
|
"write:drive": "Edit or delete your Drive files and folders"
|
||||||
"read:favorites": "View your list of favorites"
|
"read:favorites": "View your list of bookmarks"
|
||||||
"write:favorites": "Edit your list of favorites"
|
"write:favorites": "Edit your list of bookmarks"
|
||||||
"read:following": "View information on who you follow"
|
"read:following": "View information on who you follow"
|
||||||
"write:following": "Follow or unfollow other accounts"
|
"write:following": "Follow or unfollow other accounts"
|
||||||
"read:messaging": "View your chats"
|
"read:messaging": "View your chats"
|
||||||
|
|
|
@ -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] = (() => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "calckey",
|
"name": "calckey",
|
||||||
"version": "12.119.0-calc.17",
|
"version": "12.119.0-calc.18",
|
||||||
"codename": "aqua",
|
"codename": "aqua",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -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)}`;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)}`);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -232,8 +232,43 @@ const getFeed = async (acct: string) => {
|
||||||
return user && await packFeed(user);
|
return user && await packFeed(user);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// As the /@user[.json|.rss|.atom]/sub endpoint is complicated, we will use a regex to switch between them.
|
||||||
|
const reUser = new RegExp(`^/@(?<user>[^/]+?)(?:\.(?<feed>json|rss|atom))?(?:/(?<sub>[^/]+))?$`);
|
||||||
|
router.get(reUser, async (ctx, next) => {
|
||||||
|
const groups = reUser.exec(ctx.originalUrl)?.groups;
|
||||||
|
if (!groups) {
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.params = groups;
|
||||||
|
|
||||||
|
console.log(ctx, ctx.params)
|
||||||
|
if (groups.feed) {
|
||||||
|
if (groups.sub) {
|
||||||
|
await next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (groups.feed) {
|
||||||
|
case 'json':
|
||||||
|
await jsonFeed(ctx, next);
|
||||||
|
break;
|
||||||
|
case 'rss':
|
||||||
|
await rssFeed(ctx, next);
|
||||||
|
break;
|
||||||
|
case 'atom':
|
||||||
|
await atomFeed(ctx, next);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await userPage(ctx, next);
|
||||||
|
});
|
||||||
|
|
||||||
// Atom
|
// Atom
|
||||||
router.get('/@:user.atom', async ctx => {
|
const atomFeed: Router.Middleware = async ctx => {
|
||||||
const feed = await getFeed(ctx.params.user);
|
const feed = await getFeed(ctx.params.user);
|
||||||
|
|
||||||
if (feed) {
|
if (feed) {
|
||||||
|
@ -242,10 +277,10 @@ router.get('/@:user.atom', async ctx => {
|
||||||
} else {
|
} else {
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
// RSS
|
// RSS
|
||||||
router.get('/@:user.rss', async ctx => {
|
const rssFeed: Router.Middleware = async ctx => {
|
||||||
const feed = await getFeed(ctx.params.user);
|
const feed = await getFeed(ctx.params.user);
|
||||||
|
|
||||||
if (feed) {
|
if (feed) {
|
||||||
|
@ -254,10 +289,10 @@ router.get('/@:user.rss', async ctx => {
|
||||||
} else {
|
} else {
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
// JSON
|
// JSON
|
||||||
router.get('/@:user.json', async ctx => {
|
const jsonFeed: Router.Middleware = async ctx => {
|
||||||
const feed = await getFeed(ctx.params.user);
|
const feed = await getFeed(ctx.params.user);
|
||||||
|
|
||||||
if (feed) {
|
if (feed) {
|
||||||
|
@ -266,43 +301,47 @@ router.get('/@:user.json', async ctx => {
|
||||||
} else {
|
} else {
|
||||||
ctx.status = 404;
|
ctx.status = 404;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
//#region SSR (for crawlers)
|
//#region SSR (for crawlers)
|
||||||
// User
|
// User
|
||||||
router.get(['/@:user', '/@:user/:sub'], async (ctx, next) => {
|
const userPage: Router.Middleware = async (ctx, next) => {
|
||||||
const { username, host } = Acct.parse(ctx.params.user);
|
const userParam = ctx.params.user;
|
||||||
|
const subParam = ctx.params.sub;
|
||||||
|
const { username, host } = Acct.parse(userParam);
|
||||||
|
|
||||||
const user = await Users.findOneBy({
|
const user = await Users.findOneBy({
|
||||||
usernameLower: username.toLowerCase(),
|
usernameLower: username.toLowerCase(),
|
||||||
host: host ?? IsNull(),
|
host: host ?? IsNull(),
|
||||||
isSuspended: false,
|
isSuspended: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user != null) {
|
if (user === null) {
|
||||||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
|
||||||
const meta = await fetchMeta();
|
|
||||||
const me = profile.fields
|
|
||||||
? profile.fields
|
|
||||||
.filter(filed => filed.value != null && filed.value.match(/^https?:/))
|
|
||||||
.map(field => field.value)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
await ctx.render('user', {
|
|
||||||
user, profile, me,
|
|
||||||
avatarUrl: await Users.getAvatarUrl(user),
|
|
||||||
sub: ctx.params.sub,
|
|
||||||
instanceName: meta.name || 'Calckey',
|
|
||||||
icon: meta.iconUrl,
|
|
||||||
themeColor: meta.themeColor,
|
|
||||||
privateMode: meta.privateMode,
|
|
||||||
});
|
|
||||||
ctx.set('Cache-Control', 'public, max-age=15');
|
|
||||||
} else {
|
|
||||||
// リモートユーザーなので
|
|
||||||
// モデレータがAPI経由で参照可能にするために404にはしない
|
|
||||||
await next();
|
await next();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||||
|
const meta = await fetchMeta();
|
||||||
|
const me = profile.fields
|
||||||
|
? profile.fields
|
||||||
|
.filter(filed => filed.value != null && filed.value.match(/^https?:/))
|
||||||
|
.map(field => field.value)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const userDetail = {
|
||||||
|
user, profile, me,
|
||||||
|
avatarUrl: await Users.getAvatarUrl(user),
|
||||||
|
sub: subParam,
|
||||||
|
instanceName: meta.name || 'Calckey',
|
||||||
|
icon: meta.iconUrl,
|
||||||
|
themeColor: meta.themeColor,
|
||||||
|
privateMode: meta.privateMode,
|
||||||
|
};
|
||||||
|
|
||||||
|
await ctx.render('user', userDetail);
|
||||||
|
ctx.set('Cache-Control', 'public, max-age=15');
|
||||||
|
};
|
||||||
|
|
||||||
router.get('/users/:user', async ctx => {
|
router.get('/users/:user', async ctx => {
|
||||||
const user = await Users.findOneBy({
|
const user = await Users.findOneBy({
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -81,9 +81,12 @@ const bannerStyle = computed(() => {
|
||||||
top: 16px;
|
top: 16px;
|
||||||
left: 16px;
|
left: 16px;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
background: rgba(0, 0, 0, 0.7);
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
|
border-radius: 999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .status {
|
> .status {
|
||||||
|
@ -93,7 +96,9 @@ const bannerStyle = computed(() => {
|
||||||
right: 16px;
|
right: 16px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
background: rgba(0, 0, 0, 0.7);
|
-webkit-backdrop-filter: var(--blur, blur(8px));
|
||||||
|
backdrop-filter: var(--blur, blur(8px));
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
|
@ -178,6 +178,7 @@ export default defineComponent({
|
||||||
|
|
||||||
> ::v-deep(i) {
|
> ::v-deep(i) {
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
|
transform: translateY(0.1em);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:empty {
|
&:empty {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
|
<MkModal ref="modal" @click="$emit('click')" @closed="$emit('closed')">
|
||||||
<div ref="rootEl" class="hrmcaedk _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
|
<div ref="rootEl" class="hrmcaedk _narrow_" :style="{ width: `${width}px`, height: (height ? `min(${height}px, 100%)` : '100%') }">
|
||||||
<div class="header" @contextmenu="onContextmenu">
|
<div class="header" @contextmenu="onContextmenu">
|
||||||
<button v-if="history.length > 0" v-tooltip="i18n.ts.goBack" class="_button" @click="back()"><i class="ph--left-bold ph-lg"></i></button>
|
<button v-if="history.length > 0" v-tooltip="i18n.ts.goBack" class="_button" @click="back()"><i class="ph-caret-left-bold ph-lg"></i></button>
|
||||||
<span v-else style="display: inline-block; width: 20px"></span>
|
<span v-else style="display: inline-block; width: 20px"></span>
|
||||||
<span v-if="pageMetadata?.value" class="title">
|
<span v-if="pageMetadata?.value" class="title">
|
||||||
<i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i>
|
<i v-if="pageMetadata?.value.icon" class="icon" :class="pageMetadata?.value.icon"></i>
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -426,13 +426,18 @@ function readPromo() {
|
||||||
> .article {
|
> .article {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 28px 32px 18px;
|
padding: 28px 32px 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
> .avatar {
|
> .avatar {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 14px 8px 0;
|
margin: 0 14px 8px 0;
|
||||||
width: 58px;
|
width: 52px;
|
||||||
height: 58px;
|
height: 52px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
/* For some reason this breaks avatar
|
/* For some reason this breaks avatar
|
||||||
positions on notes, commenting it for now */
|
positions on notes, commenting it for now */
|
||||||
|
@ -613,7 +618,7 @@ function readPromo() {
|
||||||
margin: 0 10px 8px 0;
|
margin: 0 10px 8px 0;
|
||||||
width: 46px;
|
width: 46px;
|
||||||
height: 46px;
|
height: 46px;
|
||||||
top: calc(14px + var(--stickyTop, 0px));
|
// top: calc(14px + var(--stickyTop, 0px));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
@ -346,6 +346,11 @@ if (appearNote.replyId) {
|
||||||
|
|
||||||
> .reply-to-more {
|
> .reply-to-more {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .renote {
|
> .renote {
|
||||||
|
@ -411,8 +416,8 @@ if (appearNote.replyId) {
|
||||||
> .avatar {
|
> .avatar {
|
||||||
display: block;
|
display: block;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 58px;
|
width: 52px;
|
||||||
height: 58px;
|
height: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
|
@ -543,6 +548,11 @@ if (appearNote.replyId) {
|
||||||
|
|
||||||
> .reply {
|
> .reply {
|
||||||
border-top: solid 0.5px var(--divider);
|
border-top: solid 0.5px var(--divider);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .reply, .reply-to, .reply-to-more {
|
> .reply, .reply-to, .reply-to-more {
|
||||||
|
|
|
@ -65,6 +65,7 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
|
||||||
&.children {
|
&.children {
|
||||||
padding: 10px 0 0 16px;
|
padding: 10px 0 0 16px;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
cursor: auto;
|
||||||
|
|
||||||
&.max-width_450px {
|
&.max-width_450px {
|
||||||
padding: 10px 0 0 8px;
|
padding: 10px 0 0 8px;
|
||||||
|
@ -86,9 +87,15 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
|
||||||
> .body {
|
> .body {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
> .header {
|
> .header {
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
|
cursor: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .body {
|
> .body {
|
||||||
|
|
|
@ -57,7 +57,7 @@ const buttonsLeft = $computed(() => {
|
||||||
|
|
||||||
if (history.length > 1) {
|
if (history.length > 1) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: 'ph--left-bold ph-lg',
|
icon: 'ph-caret-left-bold ph-lg',
|
||||||
onClick: back,
|
onClick: back,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -52,42 +53,62 @@ useTooltip(buttonRef, async (showing) => {
|
||||||
}, {}, 'closed');
|
}, {}, 'closed');
|
||||||
});
|
});
|
||||||
|
|
||||||
const renote = (viaKeyboard = false, ev?: MouseEvent) => {
|
const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
|
||||||
pleaseLogin();
|
pleaseLogin();
|
||||||
if (defaultStore.state.seperateRenoteQuote) {
|
|
||||||
os.api('notes/create', {
|
const renotes = await os.api('notes/renotes', {
|
||||||
renoteId: props.note.id,
|
noteId: props.note.id,
|
||||||
visibility: props.note.visibility,
|
limit: 11,
|
||||||
});
|
});
|
||||||
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
|
|
||||||
if (el) {
|
const users = renotes.map(x => x.user.id);
|
||||||
const rect = el.getBoundingClientRect();
|
const hasRenotedBefore = users.includes($i.id);
|
||||||
const x = rect.left + (el.offsetWidth / 2);
|
|
||||||
const y = rect.top + (el.offsetHeight / 2);
|
let buttonActions = [{
|
||||||
os.popup(Ripple, { x, y }, {}, 'end');
|
text: i18n.ts.renote,
|
||||||
}
|
icon: 'ph-repeat-bold ph-lg',
|
||||||
} else {
|
danger: false,
|
||||||
os.popupMenu([{
|
action: () => {
|
||||||
text: i18n.ts.renote,
|
os.api('notes/create', {
|
||||||
icon: 'ph-repeat-bold ph-lg',
|
renoteId: props.note.id,
|
||||||
action: () => {
|
visibility: props.note.visibility,
|
||||||
os.api('notes/create', {
|
});
|
||||||
renoteId: props.note.id,
|
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
|
||||||
visibility: props.note.visibility,
|
if (el) {
|
||||||
});
|
const rect = el.getBoundingClientRect();
|
||||||
},
|
const x = rect.left + (el.offsetWidth / 2);
|
||||||
}, {
|
const y = rect.top + (el.offsetHeight / 2);
|
||||||
|
os.popup(Ripple, { x, y }, {}, 'end');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
|
||||||
|
if (!defaultStore.state.seperateRenoteQuote) {
|
||||||
|
buttonActions.push({
|
||||||
text: i18n.ts.quote,
|
text: i18n.ts.quote,
|
||||||
icon: 'ph-quotes-bold ph-lg',
|
icon: 'ph-quotes-bold ph-lg',
|
||||||
|
danger: false,
|
||||||
action: () => {
|
action: () => {
|
||||||
os.post({
|
os.post({
|
||||||
renote: props.note,
|
renote: props.note,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
}], buttonRef.value, {
|
|
||||||
viaKeyboard,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasRenotedBefore) {
|
||||||
|
buttonActions.push({
|
||||||
|
text: i18n.ts.unrenote,
|
||||||
|
icon: 'ph-trash-bold ph-lg',
|
||||||
|
danger: true,
|
||||||
|
action: () => {
|
||||||
|
os.api('notes/unrenote', {
|
||||||
|
noteId: props.note.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
os.popupMenu(buttonActions, buttonRef.value, { viaKeyboard });
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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' :
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -72,7 +72,7 @@ export const navbarItemDef = reactive({
|
||||||
},
|
},
|
||||||
favorites: {
|
favorites: {
|
||||||
title: 'favorites',
|
title: 'favorites',
|
||||||
icon: 'ph-star-bold ph-lg',
|
icon: 'ph-bookmark-simple-bold ph-lg',
|
||||||
show: computed(() => $i != null),
|
show: computed(() => $i != null),
|
||||||
to: '/my/favorites',
|
to: '/my/favorites',
|
||||||
},
|
},
|
||||||
|
|
|
@ -34,8 +34,8 @@
|
||||||
<FormSection>
|
<FormSection>
|
||||||
<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
|
<template #label>{{ i18n.ts._aboutMisskey.contributors }}</template>
|
||||||
<div class="_formLinks">
|
<div class="_formLinks">
|
||||||
<FormLink to="/@thatonecalculator@stop.voring.me"><Mfm :text="'$[sparkle @thatonecalculator@stop.voring.me (Main fork developer)]'"/></FormLink>
|
<FormLink to="/@t1c@i.calckey.cloud"><Mfm :text="'$[sparkle @t1c@i.calckey.cloud] (Main fork developer)'"/></FormLink>
|
||||||
<FormLink to="https://github.com/syuilo" external>Syuilo (Misskey developer)</FormLink>
|
<FormLink to="/@syuilo@misskey.io"><Mfm :text="'@syuilo@misskey.io (Misskey developer)'"/></FormLink>
|
||||||
<FormLink to="https://www.youtube.com/c/Henkiwashere" external>Henki (error images artist)</FormLink>
|
<FormLink to="https://www.youtube.com/c/Henkiwashere" external>Henki (error images artist)</FormLink>
|
||||||
</div>
|
</div>
|
||||||
<template #caption><MkLink url="https://codeberg.org/thatonecalculator/calckey/activity">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
|
<template #caption><MkLink url="https://codeberg.org/thatonecalculator/calckey/activity">{{ i18n.ts._aboutMisskey.allContributors }}</MkLink></template>
|
||||||
|
|
|
@ -1,35 +1,45 @@
|
||||||
<template>
|
<template>
|
||||||
<MkStickyContainer>
|
<MkStickyContainer>
|
||||||
<template #header><MkPageHeader/></template>
|
<template #header><MkPageHeader /></template>
|
||||||
<MkSpacer :content-max="800">
|
<MkSpacer :content-max="800">
|
||||||
<MkPagination ref="pagingComponent" :pagination="pagination">
|
<MkPagination ref="pagingComponent" :pagination="pagination">
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<div class="_fullinfo">
|
<div class="_fullinfo">
|
||||||
<img src="/static-assets/badges/info.png" class="_ghost" alt="Info"/>
|
<img
|
||||||
<div>{{ i18n.ts.noNotes }}</div>
|
src="/static-assets/badges/info.png"
|
||||||
</div>
|
class="_ghost"
|
||||||
</template>
|
alt="Info"
|
||||||
|
/>
|
||||||
|
<div>{{ i18n.ts.noNotes }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<template #default="{ items }">
|
<template #default="{ items }">
|
||||||
<XList v-slot="{ item }" :items="items" :direction="'down'" :no-gap="false" :ad="false">
|
<XList
|
||||||
<XNote :key="item.id" :note="item.note" :class="$style.note"/>
|
v-slot="{ item }"
|
||||||
</XList>
|
:items="items"
|
||||||
</template>
|
:direction="'down'"
|
||||||
</MkPagination>
|
:no-gap="false"
|
||||||
</MkSpacer>
|
:ad="false"
|
||||||
</MkStickyContainer>
|
>
|
||||||
|
<XNote :key="item.id" :note="item.note" :class="$style.note" />
|
||||||
|
</XList>
|
||||||
|
</template>
|
||||||
|
</MkPagination>
|
||||||
|
</MkSpacer>
|
||||||
|
</MkStickyContainer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
import MkPagination from '@/components/MkPagination.vue';
|
import MkPagination from "@/components/MkPagination.vue";
|
||||||
import XNote from '@/components/MkNote.vue';
|
import XNote from "@/components/MkNote.vue";
|
||||||
import XList from '@/components/MkDateSeparatedList.vue';
|
import XList from "@/components/MkDateSeparatedList.vue";
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from "@/i18n";
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from "@/scripts/page-metadata";
|
||||||
|
|
||||||
const pagination = {
|
const pagination = {
|
||||||
endpoint: 'i/favorites' as const,
|
endpoint: "i/favorites" as const,
|
||||||
limit: 10,
|
limit: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -37,7 +47,7 @@ const pagingComponent = ref<InstanceType<typeof MkPagination>>();
|
||||||
|
|
||||||
definePageMetadata({
|
definePageMetadata({
|
||||||
title: i18n.ts.favorites,
|
title: i18n.ts.favorites,
|
||||||
icon: 'ph-star-bold ph-lg',
|
icon: "ph-bookmark-simple-bold ph-lg",
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
<div class="other">
|
<div class="other">
|
||||||
<button v-if="$i && $i.id === post.user.id" v-tooltip="i18n.ts.edit" v-click-anime class="_button" @click="edit"><i class="ph-pencil-bold ph-lg ph-fw ph-lg"></i></button>
|
<button v-if="$i && $i.id === post.user.id" v-tooltip="i18n.ts.edit" v-click-anime class="_button" @click="edit"><i class="ph-pencil-bold ph-lg ph-fw ph-lg"></i></button>
|
||||||
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ph-repeat-bold ph-lg ph-fw ph-lg"></i></button>
|
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ph-repeat-bold ph-lg ph-fw ph-lg"></i></button>
|
||||||
<button v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network-bold ph-lg ph-fw ph-lg"></i></button>
|
<button v-if="shareAvailable()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network-bold ph-lg ph-fw ph-lg"></i></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="user">
|
<div class="user">
|
||||||
|
@ -67,6 +67,7 @@ import { url } from '@/config';
|
||||||
import { useRouter } from '@/router';
|
import { useRouter } from '@/router';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
import { shareAvailable } from '@/scripts/share-available';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="other">
|
<div class="other">
|
||||||
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ph-repeat-bold ph-lg ph-fw ph-lg"></i></button>
|
<button v-tooltip="i18n.ts.shareWithNote" v-click-anime class="_button" @click="shareWithNote"><i class="ph-repeat-bold ph-lg ph-fw ph-lg"></i></button>
|
||||||
<button v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network-bold ph-lg ph-fw ph-lg"></i></button>
|
<button v-if="shareAvailable()" v-tooltip="i18n.ts.share" v-click-anime class="_button" @click="share"><i class="ph-share-network-bold ph-lg ph-fw ph-lg"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="user">
|
<div class="user">
|
||||||
<MkAvatar :user="page.user" class="avatar"/>
|
<MkAvatar :user="page.user" class="avatar"/>
|
||||||
|
@ -81,6 +81,7 @@ import MkPagination from '@/components/MkPagination.vue';
|
||||||
import MkPagePreview from '@/components/MkPagePreview.vue';
|
import MkPagePreview from '@/components/MkPagePreview.vue';
|
||||||
import { i18n } from '@/i18n';
|
import { i18n } from '@/i18n';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||||
|
import { shareAvailable } from '@/scripts/share-available';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
pageName: string;
|
pageName: string;
|
||||||
|
@ -249,14 +250,14 @@ definePageMetadata(computed(() => page ? {
|
||||||
}
|
}
|
||||||
|
|
||||||
> .content {
|
> .content {
|
||||||
padding: 16px 0 0 0;
|
padding: 16px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .actions {
|
> .actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding: 16px 0 0 0;
|
padding: 16px 0;
|
||||||
border-top: solid 0.5px var(--divider);
|
border-top: solid 0.5px var(--divider);
|
||||||
|
|
||||||
> .like {
|
> .like {
|
||||||
|
@ -290,8 +291,8 @@ definePageMetadata(computed(() => page ? {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
> .avatar {
|
> .avatar {
|
||||||
width: 52px;
|
width: 40px;
|
||||||
height: 52px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .name {
|
> .name {
|
||||||
|
|
|
@ -8,6 +8,7 @@ import * as os from '@/os';
|
||||||
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
import copyToClipboard from '@/scripts/copy-to-clipboard';
|
||||||
import { url } from '@/config';
|
import { url } from '@/config';
|
||||||
import { noteActions } from '@/store';
|
import { noteActions } from '@/store';
|
||||||
|
import { shareAvailable } from '@/scripts/share-available';
|
||||||
|
|
||||||
export function getNoteMenu(props: {
|
export function getNoteMenu(props: {
|
||||||
note: misskey.entities.Note;
|
note: misskey.entities.Note;
|
||||||
|
@ -220,23 +221,23 @@ export function getNoteMenu(props: {
|
||||||
window.open(appearNote.url || appearNote.uri, '_blank');
|
window.open(appearNote.url || appearNote.uri, '_blank');
|
||||||
},
|
},
|
||||||
} : undefined,
|
} : undefined,
|
||||||
{
|
shareAvailable() ? {
|
||||||
icon: 'ph-share-network-bold ph-lg',
|
icon: 'ph-share-network-bold ph-lg',
|
||||||
text: i18n.ts.share,
|
text: i18n.ts.share,
|
||||||
action: share,
|
action: share,
|
||||||
},
|
} : undefined,
|
||||||
instance.translatorAvailable ? {
|
instance.translatorAvailable ? {
|
||||||
icon: 'ph-translate-bold ph-lg',
|
icon: 'ph-translate-bold ph-lg',
|
||||||
text: i18n.ts.translate,
|
text: i18n.ts.translate,
|
||||||
action: translate,
|
action: translate,
|
||||||
} : undefined,
|
} : undefined,
|
||||||
null,
|
null,
|
||||||
statePromise.then(state => state.isFavorited ? {
|
statePromise.then(state => state?.isFavorited ? {
|
||||||
icon: 'ph-star-bold ph-lg',
|
icon: 'ph-bookmark-simple-bold ph-lg',
|
||||||
text: i18n.ts.unfavorite,
|
text: i18n.ts.unfavorite,
|
||||||
action: () => toggleFavorite(false),
|
action: () => toggleFavorite(false),
|
||||||
} : {
|
} : {
|
||||||
icon: 'ph-star-bold ph-lg',
|
icon: 'ph-bookmark-simple-bold ph-lg',
|
||||||
text: i18n.ts.favorite,
|
text: i18n.ts.favorite,
|
||||||
action: () => toggleFavorite(true),
|
action: () => toggleFavorite(true),
|
||||||
}),
|
}),
|
||||||
|
|
3
packages/client/src/scripts/reduced-motion.ts
Normal file
3
packages/client/src/scripts/reduced-motion.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export function reducedMotion(): boolean {
|
||||||
|
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
}
|
6
packages/client/src/scripts/share-available.ts
Normal file
6
packages/client/src/scripts/share-available.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export function shareAvailable(): boolean {
|
||||||
|
if (navigator.share) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
@ -99,8 +99,8 @@ if ($i) {
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 2147483647;
|
z-index: 2147483647;
|
||||||
color: #ff0;
|
color: #f6c177;
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: #6e6a86;
|
||||||
padding: 4px 5px;
|
padding: 4px 5px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
@ -133,25 +133,25 @@ function getMenu() {
|
||||||
text: i18n.ts.move + '...',
|
text: i18n.ts.move + '...',
|
||||||
icon: 'ph-arrows-out-cardinal-bold ph-lg',
|
icon: 'ph-arrows-out-cardinal-bold ph-lg',
|
||||||
children: [{
|
children: [{
|
||||||
icon: 'ph--left-bold ph-lg',
|
icon: 'ph-caret-left-bold ph-lg',
|
||||||
text: i18n.ts._deck.swapLeft,
|
text: i18n.ts._deck.swapLeft,
|
||||||
action: () => {
|
action: () => {
|
||||||
swapLeftColumn(props.column.id);
|
swapLeftColumn(props.column.id);
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
icon: 'ph--right-bold ph-lg',
|
icon: 'ph-caret-right-bold ph-lg',
|
||||||
text: i18n.ts._deck.swapRight,
|
text: i18n.ts._deck.swapRight,
|
||||||
action: () => {
|
action: () => {
|
||||||
swapRightColumn(props.column.id);
|
swapRightColumn(props.column.id);
|
||||||
},
|
},
|
||||||
}, props.isStacked ? {
|
}, props.isStacked ? {
|
||||||
icon: 'ph--up-bold ph-lg',
|
icon: 'ph-caret-up-bold ph-lg',
|
||||||
text: i18n.ts._deck.swapUp,
|
text: i18n.ts._deck.swapUp,
|
||||||
action: () => {
|
action: () => {
|
||||||
swapUpColumn(props.column.id);
|
swapUpColumn(props.column.id);
|
||||||
},
|
},
|
||||||
} : undefined, props.isStacked ? {
|
} : undefined, props.isStacked ? {
|
||||||
icon: 'ph--down-bold ph-lg',
|
icon: 'ph-caret-down-bold ph-lg',
|
||||||
text: i18n.ts._deck.swapDown,
|
text: i18n.ts._deck.swapDown,
|
||||||
action: () => {
|
action: () => {
|
||||||
swapDownColumn(props.column.id);
|
swapDownColumn(props.column.id);
|
||||||
|
|
|
@ -377,6 +377,10 @@ const wallpaper = localStorage.getItem('wallpaper') != null;
|
||||||
|
|
||||||
> .button-wrapper {
|
> .button-wrapper {
|
||||||
|
|
||||||
|
> i {
|
||||||
|
transform: translateY(0.05em);
|
||||||
|
}
|
||||||
|
|
||||||
&.on {
|
&.on {
|
||||||
background-color: var(--accentedBg);
|
background-color: var(--accentedBg);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
"@shoq@newsroom.social",
|
"@shoq@newsroom.social",
|
||||||
"@pikadude@erisly.social",
|
"@pikadude@erisly.social",
|
||||||
"@sage@stop.voring.me",
|
"@sage@stop.voring.me",
|
||||||
"@sky@therian.club"
|
"@sky@therian.club",
|
||||||
|
"@panos@electricrequiem.com",
|
||||||
|
"@redhunt07@www.foxyhole.io"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
10
push-docker.sh
Executable file
10
push-docker.sh
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
sudo systemctl start docker.service
|
||||||
|
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"
|
||||||
|
read
|
||||||
|
sudo docker push thatonecalculator/calckey:$(git describe --tags --exact-match)
|
||||||
|
sudo docker push thatonecalculator/calckey:latest
|
||||||
|
sudo systemctl stop docker.service
|
Loading…
Reference in a new issue