Merge remote-tracking branch 'prismisskey/develop' into prismisskey

This commit is contained in:
mattyatea 2023-09-20 09:05:40 +09:00
commit b853625500
34 changed files with 38058 additions and 342 deletions

View file

@ -12,7 +12,7 @@
-->
## 2023.9.0 (unreleased)
## 2023.9.0-mattyaski (unreleased)
### General
- OAuth 2.0のサポート
@ -48,7 +48,10 @@
- Enhance: AiScriptで`LOCALE`として現在の設定言語を取得できるように
- Enhance: Renote自体を通報できるように
- Enhance: データセーバーモードの強化
- Safariは非対応だけど、LTEだと自動的にオンにする機能も追加 (https://developer.mozilla.org/ja/docs/Web/API/Navigator/connection)
- アイコンをblurで表示させるように
- Enhance: Renoteを管理者権限で削除可能に
- Enhance: `$[mix ]`(emojiKitchen) 記法を追加 # mattyaski独自
- `$[rainbow ]`記法が、動きのあるMFMが無効になっていても使用できるようになりました
- Playの操作を行うAPI TokenをAPIコンソールから発行できるように
- リアクションの表示サイズをより大きくできるように
@ -70,7 +73,10 @@
- Webhookのペイロードにサーバーのurlが含まれるようになりました
- Webhook設定でsecretを空に出来るように
- 使われていないアンテナの自動停止を設定可能に
- リプライをホーム投稿に # mattyaski独自
- nodeinfo 2.1対応
- Enhance: 絵文字の重複登録を不可に # mattyaski独自
- Enhance: frontendのbuildに圧縮をかけるように # mattyaski独自
- 自分へのメンション一覧を取得する際のパフォーマンスを向上
- Docker環境でjemallocを使用することでメモリ使用量を削減
- Fix: MK_ONLY_SERVERオプションを指定した際にクラッシュする問題を修正

View file

@ -1029,6 +1029,9 @@ noteIdOrUrl: "Note ID or URL"
video: "Video"
videos: "Videos"
dataSaver: "Data Saver"
cellularWithDataSaver: "Turn on Data Saver in Mobile Data Communications"
UltimatedataSaver: "Ultimate Data Saver"
cellularWithUltimateDataSaver: "Turn on Ultimate Data Saver in Mobile Data Communications"
accountMigration: "Account Migration"
accountMoved: "This user has moved to a new account:"
accountMovedShort: "This account has been migrated."
@ -1551,6 +1554,7 @@ _aboutMisskey:
contributors: "Main contributors"
allContributors: "All contributors"
source: "Source code"
forksource: "Source code for this fork"
translation: "Translate Misskey"
donate: "Donate to Misskey"
morePatrons: "We also appreciate the support of many other helpers not listed here. Thank you! 🥰"

4
locales/index.d.ts vendored
View file

@ -1032,6 +1032,9 @@ export interface Locale {
"video": string;
"videos": string;
"dataSaver": string;
"cellularWithDataSaver": string;
"UltimateDataSaver": string;
"cellularWithUltimateDataSaver": string;
"accountMigration": string;
"accountMoved": string;
"accountMovedShort": string;
@ -1656,6 +1659,7 @@ export interface Locale {
"contributors": string;
"allContributors": string;
"source": string;
"forksource": string;
"translation": string;
"donate": string;
"morePatrons": string;

View file

@ -1029,6 +1029,9 @@ noteIdOrUrl: "ートIDまたはURL"
video: "動画"
videos: "動画"
dataSaver: "データセーバー"
cellularWithDataSaver: "モバイルデータ通信でデータセーバーをオンにする"
UltimateDataSaver: "究極のデータセーバー"
cellularWithUltimateDataSaver: "モバイルデータ通信で究極のデータセーバーをオンにする"
accountMigration: "アカウントの移行"
accountMoved: "このユーザーは新しいアカウントに移行しました:"
accountMovedShort: "このアカウントは移行されています"
@ -1573,6 +1576,7 @@ _aboutMisskey:
contributors: "主なコントリビューター"
allContributors: "全てのコントリビューター"
source: "ソースコード"
forksource: "当フォークのソースコード"
translation: "Misskeyを翻訳"
donate: "Misskeyに寄付"
morePatrons: "他にも多くの方が支援してくれています。ありがとうございます🥰"

View file

@ -1,10 +1,10 @@
{
"name": "misskey",
"version": "2023.9.0-beta.8",
"version": "2023.9.0-beta.8-mattyaski.5",
"codename": "nasubi",
"repository": {
"type": "git",
"url": "https://github.com/misskey-dev/misskey.git"
"url": "https://github.com/mattyatea/misskey.git"
},
"packageManager": "pnpm@8.7.5",
"workspaces": [

View file

@ -265,7 +265,8 @@ export class FileServerService {
'avatar' in request.query ||
'static' in request.query ||
'preview' in request.query ||
'badge' in request.query
'badge' in request.query ||
'datasaver' in request.query
) {
if (!isConvertibleImage) {
// 画像でないなら404でお茶を濁す
@ -284,7 +285,7 @@ export class FileServerService {
} else {
const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) }))
.resize({
height: 'emoji' in request.query ? 128 : 320,
height: 'emoji' in request.query ? 64 : 128,
withoutEnlargement: true,
})
.webp(webpDefault);
@ -330,7 +331,28 @@ export class FileServerService {
ext: 'png',
type: 'image/png',
};
} else if (file.mime === 'image/svg+xml') {
} else if ('datasaver' in request.query){
if (!isAnimationConvertibleImage && !('static' in request.query)) {
image = {
data: fs.createReadStream(file.path),
ext: file.ext,
type: file.mime,
};
} else {
const data = (await sharpBmp(file.path, file.mime, { animated: !('static' in request.query) }))
.resize({
height: 32,
withoutEnlargement: true,
})
.webp(webpDefault);
image = {
data,
ext: 'webp',
type: 'image/webp',
};
}
}else if (file.mime === 'image/svg+xml') {
image = this.imageProcessingService.convertToWebpStream(file.path, 2048, 2048);
} else if (!file.mime.startsWith('image/') || !FILE_TYPE_BROWSERSAFE.includes(file.mime)) {
throw new StatusError('Rejected type', 403, 'Rejected type');

View file

@ -34,6 +34,7 @@ import { GlobalTimelineChannelService } from './api/stream/channels/global-timel
import { HashtagChannelService } from './api/stream/channels/hashtag.js';
import { HomeTimelineChannelService } from './api/stream/channels/home-timeline.js';
import { HybridTimelineChannelService } from './api/stream/channels/hybrid-timeline.js';
import { HybridAllTimelineChannelService } from './api/stream/channels/hybrid-all-timeline.js';
import { LocalTimelineChannelService } from './api/stream/channels/local-timeline.js';
import { QueueStatsChannelService } from './api/stream/channels/queue-stats.js';
import { ServerStatsChannelService } from './api/stream/channels/server-stats.js';
@ -79,6 +80,7 @@ import { OAuth2ProviderService } from './oauth/OAuth2ProviderService.js';
RoleTimelineChannelService,
HomeTimelineChannelService,
HybridTimelineChannelService,
HybridAllTimelineChannelService,
LocalTimelineChannelService,
QueueStatsChannelService,
ServerStatsChannelService,

View file

@ -262,6 +262,7 @@ import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete
import * as ep___notes_featured from './endpoints/notes/featured.js';
import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js';
import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js';
import * as ep___notes_hybrid_All_Timeline from './endpoints/notes/hybrid-all-timeline.js';
import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js';
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js';
@ -609,6 +610,7 @@ const $notes_favorites_delete: Provider = { provide: 'ep:notes/favorites/delete'
const $notes_featured: Provider = { provide: 'ep:notes/featured', useClass: ep___notes_featured.default };
const $notes_globalTimeline: Provider = { provide: 'ep:notes/global-timeline', useClass: ep___notes_globalTimeline.default };
const $notes_hybridTimeline: Provider = { provide: 'ep:notes/hybrid-timeline', useClass: ep___notes_hybridTimeline.default };
const $notes_hybridAllTimeline: Provider = { provide: 'ep:notes/hybrid-all-timeline', useClass: ep___notes_hybrid_All_Timeline.default };
const $notes_localTimeline: Provider = { provide: 'ep:notes/local-timeline', useClass: ep___notes_localTimeline.default };
const $notes_mentions: Provider = { provide: 'ep:notes/mentions', useClass: ep___notes_mentions.default };
const $notes_polls_recommendation: Provider = { provide: 'ep:notes/polls/recommendation', useClass: ep___notes_polls_recommendation.default };
@ -960,6 +962,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_featured,
$notes_globalTimeline,
$notes_hybridTimeline,
$notes_hybridAllTimeline,
$notes_localTimeline,
$notes_mentions,
$notes_polls_recommendation,
@ -1305,6 +1308,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
$notes_featured,
$notes_globalTimeline,
$notes_hybridTimeline,
$notes_hybridAllTimeline,
$notes_localTimeline,
$notes_mentions,
$notes_polls_recommendation,

View file

@ -262,6 +262,7 @@ import * as ep___notes_favorites_delete from './endpoints/notes/favorites/delete
import * as ep___notes_featured from './endpoints/notes/featured.js';
import * as ep___notes_globalTimeline from './endpoints/notes/global-timeline.js';
import * as ep___notes_hybridTimeline from './endpoints/notes/hybrid-timeline.js';
import * as ep___notes_hybrid_All_Timeline from './endpoints/notes/hybrid-all-timeline.js';
import * as ep___notes_localTimeline from './endpoints/notes/local-timeline.js';
import * as ep___notes_mentions from './endpoints/notes/mentions.js';
import * as ep___notes_polls_recommendation from './endpoints/notes/polls/recommendation.js';
@ -607,6 +608,7 @@ const eps = [
['notes/featured', ep___notes_featured],
['notes/global-timeline', ep___notes_globalTimeline],
['notes/hybrid-timeline', ep___notes_hybridTimeline],
['notes/hybrid-all-timeline', ep___notes_hybrid_All_Timeline],
['notes/local-timeline', ep___notes_localTimeline],
['notes/mentions', ep___notes_mentions],
['notes/polls/recommendation', ep___notes_polls_recommendation],

View file

@ -5,7 +5,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { DriveFilesRepository } from '@/models/_.js';
import type { DriveFilesRepository, EmojisRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
@ -24,6 +24,11 @@ export const meta = {
code: 'NO_SUCH_FILE',
id: 'fc46b5a4-6b92-4c33-ac66-b806659bb5cf',
},
duplicationEmojiAdd: {
message: 'This emoji is already added.',
code: 'DUPLICATION_EMOJI_ADD',
id: 'mattyaski_emoji_duplication_error',
}
},
} as const;
@ -57,7 +62,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private customEmojiService: CustomEmojiService,
private emojiEntityService: EmojiEntityService,
@ -67,6 +73,20 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const driveFile = await this.driveFilesRepository.findOneBy({ id: ps.fileId });
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
const duplicationEmoji = await this.emojisRepository.find({
where: {
name: ps.name,
},
});
duplicationEmoji.forEach(
(emoji) => {
if (emoji.name === ps.name) {
throw new ApiError(meta.errors.duplicationEmojiAdd);
}
}
)
const emoji = await this.customEmojiService.add({
driveFile,
name: ps.name,
@ -79,6 +99,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction ?? [],
});
this.moderationLogService.insertModerationLog(me, 'addEmoji', {
emojiId: emoji.id,
});

View file

@ -13,6 +13,7 @@ import { DriveService } from '@/core/DriveService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
import { ApiError } from '../../../error.js';
import {IsNull} from "typeorm";
export const meta = {
tags: ['admin'],
@ -26,6 +27,11 @@ export const meta = {
code: 'NO_SUCH_EMOJI',
id: 'e2785b66-dca3-4087-9cac-b93c541cc425',
},
duplicationEmojiAdd: {
message: 'This emoji is already added.',
code: 'DUPLICATION_EMOJI_ADD',
id: 'mattyaski_emoji_duplication_error',
}
},
res: {
@ -57,6 +63,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
private emojiEntityService: EmojiEntityService,
private idService: IdService,
private globalEventService: GlobalEventService,
@ -69,6 +76,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchEmoji);
}
const duplicationEmoji = await this.emojisRepository.find({
where: {
name: emoji.name,
host: IsNull()
},
});
duplicationEmoji.forEach(
(_emoji) => {
if (_emoji.name === emoji.name) {
throw new ApiError(meta.errors.duplicationEmojiAdd);
}
}
)
let driveFile: MiDriveFile;
try {

View file

@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
import type { DriveFilesRepository } from '@/models/_.js';
import type { DriveFilesRepository , EmojisRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
@ -32,6 +32,11 @@ export const meta = {
code: 'SAME_NAME_EMOJI_EXISTS',
id: '7180fe9d-1ee3-bff9-647d-fe9896d2ffb8',
},
duplicationEmojiAdd: {
message: 'This emoji is already added.',
code: 'DUPLICATION_EMOJI_ADD',
id: 'mattyaski_emoji_duplication_error',
}
},
} as const;
@ -64,7 +69,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
constructor(
@Inject(DI.driveFilesRepository)
private driveFilesRepository: DriveFilesRepository,
private customEmojiService: CustomEmojiService,
) {
super(meta, paramDef, async (ps, me) => {

View file

@ -17,6 +17,7 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../error.js';
import {noteVisibilities} from "@/types.js";
export const meta = {
tags: ['notes'],
@ -232,7 +233,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
}
}
let visibility = ps.visibility;
let reply: MiNote | null = null;
if (ps.replyId != null) {
// Fetch reply
@ -243,7 +244,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} else if (reply.renoteId && !reply.text && !reply.fileIds && !reply.hasPoll) {
throw new ApiError(meta.errors.cannotReplyToPureRenote);
}
// ノートがリプライでパブリック投稿の場合はホームにする
if (ps.visibility != 'home' && ps.visibility!== 'followers' && ps.visibility!=='specified' ){
visibility = 'home';
}
// Check blocking
if (reply.userId !== me.id) {
const blockExist = await this.blockingsRepository.exist({
@ -292,7 +296,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
cw: ps.cw,
localOnly: ps.localOnly,
reactionAcceptance: ps.reactionAcceptance,
visibility: ps.visibility,
visibility,
visibleUsers,
channel,
apMentions: ps.noExtractMentions ? [] : undefined,

View file

@ -0,0 +1,126 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
import type { NotesRepository } from '@/models/index.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
import { ApiError } from '../../error.js';
export const meta = {
tags: ['notes'],
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
ref: 'Note',
},
},
errors: {
ltlDisabled: {
message: 'hybrid Local timeline has been disabled.',
code: 'LTL_DISABLED',
id: '45a6eb02-7695-4393-b023-dd3be9aaaefd',
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
withFiles: { type: 'boolean', default: false },
withReplies: { type: 'boolean', default: false },
fileType: { type: 'array', items: {
type: 'string',
} },
excludeNsfw: { type: 'boolean', default: false },
limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
sinceId: { type: 'string', format: 'misskey:id' },
untilId: { type: 'string', format: 'misskey:id' },
sinceDate: { type: 'integer' },
untilDate: { type: 'integer' },
},
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const policies = await this.roleService.getUserPolicies(me ? me.id : null);
if (!policies.ltlAvailable) {
throw new ApiError(meta.errors.ltlDisabled);
}
//#region Construct query
const query = this.queryService.makePaginationQuery(this.notesRepository.createQueryBuilder('note'),
ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
.andWhere('note.id > :minId', { minId: this.idService.genId(new Date(Date.now() - (1000 * 60 * 60 * 24 * 10))) }) // 10日前まで
.andWhere('(note.visibility = \'home\') AND (note.userHost IS NULL)')
.innerJoinAndSelect('note.user', 'user')
.leftJoinAndSelect('note.reply', 'reply')
.leftJoinAndSelect('note.renote', 'renote')
.leftJoinAndSelect('reply.user', 'replyUser')
.leftJoinAndSelect('renote.user', 'renoteUser');
this.queryService.generateChannelQuery(query, me);
this.queryService.generateRepliesQuery(query, ps.withReplies, me);
this.queryService.generateVisibilityQuery(query, me);
if (me) this.queryService.generateMutedUserQuery(query, me);
if (me) this.queryService.generateMutedNoteQuery(query, me);
if (me) this.queryService.generateBlockedUserQuery(query, me);
if (me) this.queryService.generateMutedUserRenotesQueryForNotes(query, me);
if (ps.withFiles) {
query.andWhere('note.fileIds != \'{}\'');
}
if (ps.fileType != null) {
query.andWhere('note.fileIds != \'{}\'');
query.andWhere(new Brackets(qb => {
for (const type of ps.fileType!) {
const i = ps.fileType!.indexOf(type);
qb.orWhere(`:type${i} = ANY(note.attachedFileTypes)`, { [`type${i}`]: type });
}
}));
if (ps.excludeNsfw) {
query.andWhere('note.cw IS NULL');
query.andWhere('0 = (SELECT COUNT(*) FROM drive_file df WHERE df.id = ANY(note."fileIds") AND df."isSensitive" = TRUE)');
}
}
//#endregion
const timeline = await query.limit(ps.limit).getMany();
process.nextTick(() => {
if (me) {
this.activeUsersChart.read(me);
}
});
return await this.noteEntityService.packMany(timeline, me);
});
}
}

View file

@ -48,7 +48,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
throw err;
});
return await this.noteEntityService.pack(note, me, {
detail: true,
});

View file

@ -19,7 +19,7 @@ import { AntennaChannelService } from './channels/antenna.js';
import { DriveChannelService } from './channels/drive.js';
import { HashtagChannelService } from './channels/hashtag.js';
import { RoleTimelineChannelService } from './channels/role-timeline.js';
import { HybridAllTimelineChannelService } from './channels/hybrid-all-timeline.js';
@Injectable()
export class ChannelsService {
constructor(
@ -27,6 +27,7 @@ export class ChannelsService {
private homeTimelineChannelService: HomeTimelineChannelService,
private localTimelineChannelService: LocalTimelineChannelService,
private hybridTimelineChannelService: HybridTimelineChannelService,
private hybridAllTimelineChannelService: HybridAllTimelineChannelService,
private globalTimelineChannelService: GlobalTimelineChannelService,
private userListChannelService: UserListChannelService,
private hashtagChannelService: HashtagChannelService,
@ -47,6 +48,7 @@ export class ChannelsService {
case 'homeTimeline': return this.homeTimelineChannelService;
case 'localTimeline': return this.localTimelineChannelService;
case 'hybridTimeline': return this.hybridTimelineChannelService;
case 'hybridAllTimeline': return this.hybridAllTimelineChannelService;
case 'globalTimeline': return this.globalTimelineChannelService;
case 'userList': return this.userListChannelService;
case 'hashtag': return this.hashtagChannelService;

View file

@ -0,0 +1,119 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Injectable } from '@nestjs/common';
import { checkWordMute } from '@/misc/check-word-mute.js';
import { isUserRelated } from '@/misc/is-user-related.js';
import type { Packed } from '@/misc/json-schema.js';
import { MetaService } from '@/core/MetaService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import Channel from '../channel.js';
class HybridAllTimelineChannel extends Channel {
public readonly chName = 'hybridAllTimeline';
public static shouldShare = true;
public static requireCredential = false;
private withReplies: boolean;
constructor(
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
id: string,
connection: Channel['connection'],
) {
super(id, connection);
//this.onNote = this.onNote.bind(this);
}
@bindThis
public async init(params: any) {
const policies = await this.roleService.getUserPolicies(this.user ? this.user.id : null);
if (!policies.ltlAvailable) return;
this.withReplies = params.withReplies as boolean;
// Subscribe events
this.subscriber.on('notesStream', this.onNote);
}
@bindThis
private async onNote(note: Packed<'Note'>) {
if (note.user.host !== null) return;
if (note.visibility === "public") return;
if (note.channelId != null && !this.followingChannels.has(note.channelId)) return;
// リプライなら再pack
if (note.replyId != null) {
note.reply = await this.noteEntityService.pack(note.replyId, this.user, {
detail: true,
});
}
// Renoteなら再pack
if (note.renoteId != null) {
note.renote = await this.noteEntityService.pack(note.renoteId, this.user, {
detail: true,
});
}
// 関係ない返信は除外
if (note.reply && this.user && !this.withReplies) {
const reply = note.reply;
// 「チャンネル接続主への返信」でもなければ、「チャンネル接続主が行った返信」でもなければ、「投稿者の投稿者自身への返信」でもない場合
if (reply.userId !== this.user.id && note.userId !== this.user.id && reply.userId !== note.userId) return;
}
// 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoMeMuting)) return;
// 流れてきたNoteがブロックされているユーザーが関わるものだったら無視する
if (isUserRelated(note, this.userIdsWhoBlockingMe)) return;
if (note.renote && !note.text && isUserRelated(note, this.userIdsWhoMeMutingRenotes)) return;
// 流れてきたNoteがミュートすべきNoteだったら無視する
// TODO: 将来的には、単にMutedNoteテーブルにレコードがあるかどうかで判定したい(以下の理由により難しそうではある)
// 現状では、ワードミュートにおけるMutedNoteレコードの追加処理はストリーミングに流す処理と並列で行われるため、
// レコードが追加されるNoteでも追加されるより先にここのストリーミングの処理に到達することが起こる。
// そのためレコードが存在するかのチェックでは不十分なので、改めてcheckWordMuteを呼んでいる
if (this.userProfile && await checkWordMute(note, this.user, this.userProfile.mutedWords)) return;
this.connection.cacheNote(note);
this.send('note', note);
}
@bindThis
public dispose() {
// Unsubscribe events
this.subscriber.off('notesStream', this.onNote);
}
}
@Injectable()
export class HybridAllTimelineChannelService {
public readonly shouldShare = HybridAllTimelineChannel.shouldShare;
public readonly requireCredential = HybridAllTimelineChannel.requireCredential;
constructor(
private metaService: MetaService,
private roleService: RoleService,
private noteEntityService: NoteEntityService,
) {
}
@bindThis
public create(id: string, connection: Channel['connection']): HybridAllTimelineChannel {
return new HybridAllTimelineChannel(
this.metaService,
this.roleService,
this.noteEntityService,
id,
connection,
);
}
}

View file

@ -130,6 +130,7 @@
"storybook": "7.4.1",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"summaly": "github:misskey-dev/summaly",
"vite-plugin-compression2": "^0.10.4",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "0.34.4",
"vitest-fetch-mock": "0.2.2",

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.root" :style="bg">
<img v-if="faviconUrl" :class="$style.icon" :src="faviconUrl"/>
<img v-if="faviconUrl && !defaultStore.state.enableUltimateDataSaverMode" :class="$style.icon" :src="faviconUrl"/>
<div :class="$style.name">{{ instance.name }}</div>
</div>
</template>
@ -15,6 +15,7 @@ import { } from 'vue';
import { instanceName } from '@/config';
import { instance as Instance } from '@/instance';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy';
import {defaultStore} from "@/store";
const props = defineProps<{
instance?: {

View file

@ -88,6 +88,15 @@ if (props.src === 'antenna') {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on('note', prepend);
} else if (props.src === 'all') {
endpoint = 'notes/hybrid-all-timeline';
query = {
withReplies: defaultStore.state.showTimelineReplies,
};
connection = stream.useChannel('hybridAllTimeline', {
withReplies: defaultStore.state.showTimelineReplies,
});
connection.on('note', prepend);
} else if (props.src === 'global') {
endpoint = 'notes/global-timeline';
query = {

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<component :is="link ? MkA : 'span'" v-user-preview="preview ? user.id : undefined" v-bind="bound" class="_noSelect" :class="[$style.root, { [$style.animation]: animation, [$style.cat]: user.isCat, [$style.square]: squareAvatars }]" :style="{ color }" :title="acct(user)" @click="onClick">
<MkImgWithBlurhash :class="$style.inner" :src="url" :hash="user?.avatarBlurhash" :cover="true" :onlyAvgColor="true"/>
<MkImgWithBlurhash :class="$style.inner" :src="defaultStore.state.enableUltimateDataSaverMode ? undefined : url" :hash="user?.avatarBlurhash" :cover="true" :onlyAvgColor="true"/>
<MkUserOnlineIndicator v-if="indicator" :class="$style.indicator" :user="user"/>
<div v-if="user.isCat" :class="[$style.ears]">
<div :class="$style.earLeft">

View file

@ -38,13 +38,20 @@ const rawUrl = computed(() => {
const url = computed(() => {
if (rawUrl.value == null) return null;
const useOriginalSize = props.useOriginalSize;
const enableDataSaverMode = defaultStore.state.enableUltimateDataSaverMode;
let datasaver_result ;
if (enableDataSaverMode) {
datasaver_result = useOriginalSize ? undefined : 'datasaver';
} else {
datasaver_result = useOriginalSize ? undefined : 'emoji';
}
const proxied =
(rawUrl.value.startsWith('/emoji/') || (props.useOriginalSize && isLocal.value))
? rawUrl.value
: getProxiedImageUrl(
rawUrl.value,
props.useOriginalSize ? undefined : 'emoji',
datasaver_result,
false,
true,
);

View file

@ -0,0 +1,46 @@
<template>
<span v-if="errored">{{ alt }}</span>
<img v-else :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :src="url" :alt="alt" :title="alt" decoding="async" @error="errored = true" @load="errored = false"/>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
const props = defineProps<{
name: string;
normal?: boolean;
url: string;
}>();
const rawUrl = computed(() => props.url);
const url = computed(() => rawUrl.value);
const alt = computed(() => props.name);
let errored = $ref(url.value == null);
</script>
<style lang="scss" module>
.root {
height: 2em;
vertical-align: middle;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.2);
}
}
.normal {
height: 1.25em;
vertical-align: -0.25em;
&:hover {
transform: none;
}
}
.noStyle {
height: auto !important;
}
</style>

View file

@ -1,8 +1,3 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { VNode, h } from 'vue';
import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js';
@ -11,12 +6,14 @@ import MkLink from '@/components/MkLink.vue';
import MkMention from '@/components/MkMention.vue';
import MkEmoji from '@/components/global/MkEmoji.vue';
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
import MkEmojiKitchen from '@/components/global/MkEmojiKitchen.vue';
import MkCode from '@/components/MkCode.vue';
import MkGoogle from '@/components/MkGoogle.vue';
import MkSparkle from '@/components/MkSparkle.vue';
import MkA from '@/components/global/MkA.vue';
import { host } from '@/config';
import { defaultStore } from '@/store';
import { mixEmoji } from '@/scripts/emojiKitchen/emojiMixer';
const QUOTE_STYLE = `
display: block;
@ -27,6 +24,38 @@ border-left: solid 3px var(--fg);
opacity: 0.7;
`.split('\n').join(' ');
const colorRegexp = /^([0-9a-f]{3,4}?|[0-9a-f]{6}?|[0-9a-f]{8}?)$/i;
function checkColorHex(text: string) {
return colorRegexp.test(text);
}
const gradientCounterRegExp = /^(color|step)(\d+)/;
function toGradientText(args: Record<string, string>) {
const colors: { index: number; step?: string, color?: string }[] = [];
for (const k in args) {
const matches = k.match(gradientCounterRegExp);
if (matches == null) continue;
const mindex = parseInt(matches[2]);
let i = colors.findIndex(v => v.index === mindex);
if (i === -1) {
i = colors.length;
colors.push({ index: mindex });
}
colors[i][matches[1]] = args[k];
}
let deg = parseFloat(args.deg || '90');
let res = `linear-gradient(${deg}deg`;
for (const colorProp of colors.sort((a, b) => a.index - b.index)) {
let color = colorProp.color;
if (!color || !checkColorHex(color)) color = 'f00';
let step = parseFloat(colorProp.step ?? '');
let stepText = isNaN(step) ? '' : ` ${step}%`;
res += `, #${color}${stepText}`;
}
return res + ')';
}
export default function(props: {
text: string;
plain?: boolean;
@ -44,7 +73,7 @@ export default function(props: {
const ast = (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
const validTime = (t: string | null | undefined) => {
if (t == null) return null;
if (t == null || typeof t === 'boolean') return null;
return t.match(/^[0-9.]+s$/) ? t : null;
};
@ -170,18 +199,15 @@ export default function(props: {
break;
}
case 'blur': {
const radius = parseFloat(token.props.args.rad ?? '6');
return h('span', {
class: '_mfm_blur_',
style: `--blur-px: ${radius}px;`
}, genEl(token.children, scale));
}
case 'rainbow': {
if (!useAnim) {
return h('span', {
class: '_mfm_rainbow_fallback_',
}, genEl(token.children, scale));
}
const speed = validTime(token.props.args.speed) ?? '1s';
style = `animation: mfm-rainbow ${speed} linear infinite;`;
style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
break;
}
case 'sparkle': {
@ -192,7 +218,23 @@ export default function(props: {
}
case 'rotate': {
const degrees = parseFloat(token.props.args.deg ?? '90');
style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
let rotateText = `rotate(${degrees}deg)`;
if (!token.props.args.deg && (token.props.args.x || token.props.args.y || token.props.args.z)) {
rotateText = '';
}
if (token.props.args.x) {
const degrees = parseFloat(token.props.args.x ?? '0');
rotateText += ` rotateX(${degrees}deg)`;
}
if (token.props.args.y) {
const degrees = parseFloat(token.props.args.y ?? '0');
rotateText += ` rotateY(${degrees}deg)`;
}
if (token.props.args.z) {
const degrees = parseFloat(token.props.args.z ?? '0');
rotateText += ` rotateZ(${degrees}deg)`;
}
style = `transform: ${rotateText}; transform-origin: center center;`;
break;
}
case 'position': {
@ -213,18 +255,101 @@ export default function(props: {
scale = scale * Math.max(x, y);
break;
}
case 'skew': {
if (!defaultStore.state.advancedMfm) {
style = '';
break;
}
const x = parseFloat(token.props.args.x ?? '0');
const y = parseFloat(token.props.args.y ?? '0');
style = `transform: skew(${x}deg, ${y}deg);`;
break;
}
case 'fgg': {
if (!defaultStore.state.advancedMfm) break;
style = `-webkit-background-clip: text; -webkit-text-fill-color: transparent; background-image: ${toGradientText(token.props.args)};`
break;
}
case 'fg': {
let color = token.props.args.color;
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
if (!checkColorHex(color)) color = 'f00';
style = `color: #${color};`;
break;
}
case 'bgg': {
if (!defaultStore.state.advancedMfm) break;
style = `background-image: ${toGradientText(token.props.args)};`
break;
}
case 'bg': {
let color = token.props.args.color;
if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
if (!checkColorHex(color)) color = 'f00';
style = `background-color: #${color};`;
break;
}
case 'clip': {
if (!defaultStore.state.advancedMfm) break;
let path = '';
if (token.props.args.circle) {
const percent = parseFloat(token.props.args.circle ?? '');
const percentText = isNaN(percent) ? '' : `${percent}%`;
path = `circle(${percentText})`;
}
else {
const top = parseFloat(token.props.args.t ?? '0');
const bottom = parseFloat(token.props.args.b ?? '0');
const left = parseFloat(token.props.args.l ?? '0');
const right = parseFloat(token.props.args.r ?? '0');
path = `inset(${top}% ${right}% ${bottom}% ${left}%)`;
}
style = `clip-path: ${path};`;
break;
}
case 'move': {
const speed = validTime(token.props.args.speed) ?? '1s';
const fromX = parseFloat(token.props.args.fromx ?? '0');
const fromY = parseFloat(token.props.args.fromy ?? '0');
const toX = parseFloat(token.props.args.tox ?? '0');
const toY = parseFloat(token.props.args.toy ?? '0');
const ease =
token.props.args.ease ? 'ease' :
token.props.args.easein ? 'ease-in' :
token.props.args.easeout ? 'ease-out' :
token.props.args.easeinout ? 'ease-in-out' :
'linear';
const delay = validTime(token.props.args.delay) ?? '0s';
const direction =
token.props.args.rev && token.props.args.once ? 'reverse' :
token.props.args.rev ? 'alternate-reverse' :
token.props.args.once ? 'normal' :
'alternate';
style = useAnim ? `--move-fromX: ${fromX}em; --move-fromY: ${fromY}em; --move-toX: ${toX}em; --move-toY: ${toY}em; animation: ${speed} ${ease} ${delay} infinite ${direction} mfm-move;` : '';
break;
}
case 'mix': {
const ch = token.children;
if (ch.length != 2 || ch.some(c => c.type !== 'unicodeEmoji')) {
style = null;
break;
}
const emoji1 = ch[0].props.emoji;
const emoji2 = ch[1].props.emoji;
const mixedEmojiUrl = mixEmoji(emoji1, emoji2);
if (!mixedEmojiUrl) {
style = null;
break;
}
return h(MkEmojiKitchen, {
key: Math.random(),
name: emoji1 + emoji2,
normal: props.plain,
url: mixedEmojiUrl
});
}
}
if (style == null) {
return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);

View file

@ -34,6 +34,11 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._aboutMisskey.source }}
<template #suffix>GitHub</template>
</FormLink>
<FormLink to="https://github.com/mattyatea/misskey" external>
<template #icon><i class="ti ti-code"></i></template>
{{ i18n.ts._aboutMisskey.forksource }}
<template #suffix>GitHub</template>
</FormLink>
<FormLink to="https://crowdin.com/project/misskey" external>
<template #icon><i class="ti ti-language-hiragana"></i></template>
{{ i18n.ts._aboutMisskey.translation }}

View file

@ -113,7 +113,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch>
<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch>
<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
<MkSwitch v-model="enableDataSaverMode">{{ i18n.ts.dataSaver }}</MkSwitch>
<MkSwitch :disabled="enableUltimateDataSaverMode || enableCellularWithUltimateDataSaver" v-model="enableDataSaverMode">{{ i18n.ts.dataSaver }}</MkSwitch>
<MkSwitch :disabled="enableUltimateDataSaverMode || enableCellularWithUltimateDataSaver" v-model="enableCellularWithDataSaver">{{ i18n.ts.cellularWithDataSaver }}</MkSwitch>
<MkSwitch v-model="enableUltimateDataSaverMode">{{ i18n.ts.UltimateDataSaver }}</MkSwitch>
<MkSwitch v-model="enableCellularWithUltimateDataSaver">{{ i18n.ts.cellularWithUltimateDataSaver }}</MkSwitch>
</div>
<div>
<MkRadios v-model="emojiStyle">
@ -227,7 +230,10 @@ const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
const enableDataSaverMode = computed(defaultStore.makeGetterSetter('enableDataSaverMode'));
const enableDataSaverMode = computed(defaultStore.makeGetterSetter('enableDataSaverMode')) ;
const enableCellularWithDataSaver = computed(defaultStore.makeGetterSetter('enableCellularWithDataSaver'));
const enableUltimateDataSaverMode = computed(defaultStore.makeGetterSetter('enableUltimateDataSaverMode'))
const enableCellularWithUltimateDataSaver = computed(defaultStore.makeGetterSetter('enableCellularWithUltimateDataSaver'));
const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
@ -242,6 +248,8 @@ const notificationPosition = computed(defaultStore.makeGetterSetter('notificatio
const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
const showTimelineReplies = computed(defaultStore.makeGetterSetter('showTimelineReplies'));
watch(lang, () => {
miLocalStorage.setItem('lang', lang.value as string);
miLocalStorage.removeItem('locale');

View file

@ -47,6 +47,7 @@ const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue'));
const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
const isAdmin = ($i != null && $i.isAdmin);
const keymap = {
't': focus,
};
@ -102,7 +103,7 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
os.popupMenu(items, ev.currentTarget ?? ev.target);
}
function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global'): void {
function saveSrc(newSrc: 'home' | 'local' | 'social' | 'global' | 'all'): void {
defaultStore.set('tl', {
...defaultStore.state.tl,
src: newSrc,
@ -145,6 +146,11 @@ const headerTabs = $computed(() => [{
title: i18n.ts._timelines.global,
icon: 'ti ti-whirl',
iconOnly: true,
}] : []), ...(isAdmin ? [{
key: 'all',
title: 'all',
icon: 'ti ti-whirl',
iconOnly: true,
}] : []), {
icon: 'ti ti-list',
title: i18n.ts.lists,

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,67 @@
import * as data from './emojiData';
const mixEmojiUrl = (r, c) => {
let padZeros = r < 20220500; // Revisions before 0522 had preceding zeros
c[0] = c[0].split(/-/g).map(s => padZeros ? s.padStart(4, "0") : s).join("-u");
c[1] = c[1].split(/-/g).map(s => padZeros ? s.padStart(4, "0") : s).join("-u");
return `https://www.gstatic.com/android/keyboard/emojikitchen/${r}/u${c[0]}/u${c[0]}_u${c[1]}.png`;
};
const convertBase = (value, from_base, to_base) => {
value = value.toString();
var range = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ+/'.split('');
var from_range = range.slice(0, from_base);
var to_range = range.slice(0, to_base);
var dec_value = value.split('').reverse().reduce(function (carry, digit, index) {
if (from_range.indexOf(digit) === -1) throw new Error('Invalid digit `' + digit + '` for base ' + from_base + '.');
return carry += from_range.indexOf(digit) * (Math.pow(from_base, index));
}, 0);
var new_value = '';
while (dec_value > 0) {
new_value = to_range[dec_value % to_base] + new_value;
dec_value = (dec_value - (dec_value % to_base)) / to_base;
}
return new_value || '0';
};
const emojiSplit = String.fromCodePoint(0x200d);
const hexEncodeEmoji = (chr) => {
if (chr.length === 3) return hexEncodeEmoji(chr.slice(0, 2)) + '-' + hexEncodeEmoji(chr.slice(2, chr.length));
else if (chr.length === 2) {
const hi = chr.charCodeAt(0);
const lo = chr.charCodeAt(1);
if (0xD800 <= hi && hi < 0xDC00 && 0xDC00 <= lo && lo < 0xE000) {
return (0x10000 + (hi - 0xD800) * 0x400 + (lo - 0xDC00)).toString(16);
}
return hi.toString(16) + '-' + lo.toString(16);
}
else if (chr.length === 1) {
return chr.charCodeAt(0).toString(16);
}
else {
const sp = chr.split(emojiSplit);
if (sp.length !== 2) return '';
return hexEncodeEmoji(sp[0]) + '-200d-' + hexEncodeEmoji(sp[1]);
}
};
const pairsMatchingMap = match => {
const mv = match[0];
let [d, c1, c2] = mv.split('.');
c1 = data.points[convertBase(c1, 64, 10)];
c2 = data.points[convertBase(c2, 64, 10)];
d = data.revisions[convertBase(d, 64, 10)];
return mixEmojiUrl(d, [c1, c2]);
};
export const mixEmoji = (emoji1, emoji2) => {
const encordedEmoji1 = convertBase(data.points.indexOf(hexEncodeEmoji(emoji1)), 10, 64);
const encordedEmoji2 = convertBase(data.points.indexOf(hexEncodeEmoji(emoji2)), 10, 64);
return [
...data.pairs.matchAll(new RegExp("^.*\\." + encordedEmoji1 + "\\." + encordedEmoji2 + "\\.$", "gm")),
...data.pairs.matchAll(new RegExp("^.*\\." + encordedEmoji2 + "\\." + encordedEmoji1 + "\\.$", "gm"))
].map(pairsMatchingMap).pop();
};

View file

@ -1,6 +1 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'font', 'blur', 'rainbow', 'sparkle', 'rotate'];
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'skew', 'position', 'fg', 'bg', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'fgg', 'bgg', 'clip', 'move', 'mix'];

View file

@ -206,6 +206,18 @@ export const defaultStore = markRaw(new Storage('base', {
enableDataSaverMode: {
where: 'device',
default: false,
},
enableUltimateDataSaverMode: {
where: 'device',
default: false,
},
enableCellularWithDataSaver: {
where: 'device',
default: false,
},
enableCellularWithUltimateDataSaver: {
where: 'device',
default: false,
},
disableShowingAnimatedImages: {
where: 'device',

View file

@ -4,10 +4,11 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.root">
<div :class="$style.root">
<XSidebar v-if="!isMobile" :class="$style.sidebar"/>
<MkStickyContainer ref="contents" :class="$style.contents" style="container-type: inline-size;" @contextmenu.stop="onContextmenu">
<MkStickyContainer ref="contents" :class="$style.contents" style="container-type: inline-size;"
@contextmenu.stop="onContextmenu">
<template #header>
<div>
<XAnnouncements v-if="$i" :class="$style.announcements"/>
@ -22,14 +23,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<XWidgets/>
</div>
<button v-if="!isDesktop && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button>
<button v-if="!isDesktop && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true">
<i class="ti ti-apps"></i></button>
<div v-if="isMobile" ref="navFooter" :class="$style.nav">
<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
<button :class="$style.navButton" class="_button" @click="mainRouter.currentRoute.value.name === 'index' ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button>
<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"><i :class="$style.navButtonIcon" class="ti ti-bell"></i><span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
<button :class="$style.navButton" class="_button" @click="widgetsShowing = true"><i :class="$style.navButtonIcon" class="ti ti-apps"></i></button>
<button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button>
<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i
:class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated"
:class="$style.navButtonIndicator"><i
class="_indicatorCircle"></i></span></button>
<button :class="$style.navButton" class="_button"
@click="mainRouter.currentRoute.value.name === 'index' ? top() : mainRouter.push('/')"><i
:class="$style.navButtonIcon" class="ti ti-home"></i></button>
<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')"><i
:class="$style.navButtonIcon" class="ti ti-bell"></i><span v-if="$i?.hasUnreadNotification"
:class="$style.navButtonIndicator"><i
class="_indicatorCircle"></i></span></button>
<button :class="$style.navButton" class="_button" @click="widgetsShowing = true"><i :class="$style.navButtonIcon"
class="ti ti-apps"></i>
</button>
<button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon"
class="ti ti-pencil"></i></button>
</div>
<Transition
@ -80,32 +93,33 @@ SPDX-License-Identifier: AGPL-3.0-only
:leaveToClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveTo : ''"
>
<div v-if="widgetsShowing" :class="$style.widgetsDrawer">
<button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i class="ti ti-x"></i></button>
<button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i
class="ti ti-x"></i></button>
<XWidgets/>
</div>
</Transition>
<XCommon/>
</div>
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent, provide, onMounted, computed, ref, ComputedRef, watch, shallowRef, Ref } from 'vue';
import {defineAsyncComponent, provide, onMounted, computed, ref, ComputedRef, watch, shallowRef, Ref} from 'vue';
import XCommon from './_common_/common.vue';
import type MkStickyContainer from '@/components/global/MkStickyContainer.vue';
import { instanceName } from '@/config';
import {instanceName} from '@/config';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import * as os from '@/os';
import { defaultStore } from '@/store';
import { navbarItemDef } from '@/navbar';
import { i18n } from '@/i18n';
import { $i } from '@/account';
import { mainRouter } from '@/router';
import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata';
import { deviceKind } from '@/scripts/device-kind';
import { miLocalStorage } from '@/local-storage';
import { CURRENT_STICKY_BOTTOM } from '@/const';
import { useScrollPositionManager } from '@/nirax';
import {defaultStore} from '@/store';
import {navbarItemDef} from '@/navbar';
import {i18n} from '@/i18n';
import {$i} from '@/account';
import {mainRouter} from '@/router';
import {PageMetadata, provideMetadataReceiver} from '@/scripts/page-metadata';
import {deviceKind} from '@/scripts/device-kind';
import {miLocalStorage} from '@/local-storage';
import {CURRENT_STICKY_BOTTOM} from '@/const';
import {useScrollPositionManager} from '@/nirax';
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
@ -115,6 +129,33 @@ const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announce
const DESKTOP_THRESHOLD = 1100;
const MOBILE_THRESHOLD = 500;
onMounted(() => {
if (
window.navigator.connection.type === "cellular" &&
!defaultStore.state.enableUltimateDataSaverMode &&
defaultStore.state.enableCellularWithUltimateDataSaver
) {
defaultStore.state.enableDataSaverMode = true;
defaultStore.state.enableUltimateDataSaverMode = true;
} else if (window.navigator.connection.type !== "cellular" && window.navigator.connection.type !== "undefined" && defaultStore.state.enableDataSaverMode && defaultStore.state.enableCellularWithDataSaver) {
defaultStore.state.enableDataSaverMode = false;
defaultStore.state.enableUltimateDataSaverMode = true;
}
if (
window.navigator.connection.type === "cellular" &&
!defaultStore.state.enableDataSaverMode &&
defaultStore.state.enableCellularWithDataSaver
) {
defaultStore.state.enableDataSaverMode = true;
} else if (window.navigator.connection.type !== "cellular" && window.navigator.connection.type !== "undefined" && defaultStore.state.enableDataSaverMode && defaultStore.state.enableCellularWithDataSaver) {
defaultStore.state.enableDataSaverMode = false;
}
if (defaultStore.state.enableUltimateDataSaverMode) {
defaultStore.state.enableDataSaverMode = true;
}
});
// UI deviceKind === 'desktop'
const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
@ -177,7 +218,7 @@ onMounted(() => {
if (!isDesktop.value) {
window.addEventListener('resize', () => {
if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true;
}, { passive: true });
}, {passive: true});
}
});
@ -262,6 +303,7 @@ $widgets-hide-threshold: 1090px;
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_menuDrawerBg_enterFrom,
.transition_menuDrawerBg_leaveTo {
opacity: 0;
@ -273,6 +315,7 @@ $widgets-hide-threshold: 1090px;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_menuDrawer_enterFrom,
.transition_menuDrawer_leaveTo {
opacity: 0;
@ -284,6 +327,7 @@ $widgets-hide-threshold: 1090px;
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_widgetsDrawerBg_enterFrom,
.transition_widgetsDrawerBg_leaveTo {
opacity: 0;
@ -295,6 +339,7 @@ $widgets-hide-threshold: 1090px;
transform: translateX(0);
transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1), opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_widgetsDrawer_enterFrom,
.transition_widgetsDrawer_leaveTo {
opacity: 0;

View file

@ -1,7 +1,7 @@
import path from 'path';
import pluginReplace from '@rollup/plugin-replace';
import pluginVue from '@vitejs/plugin-vue';
import { type UserConfig, defineConfig } from 'vite';
import {type UserConfig, defineConfig} from 'vite';
// @ts-expect-error https://github.com/sxzz/unplugin-vue-macros/issues/257#issuecomment-1410752890
import ReactivityTransform from '@vue-macros/reactivity-transform/vite';
@ -9,6 +9,7 @@ import locales from '../../locales';
import meta from '../../package.json';
import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name';
import pluginJson5 from './vite.json5';
import compression from "vite-plugin-compression2";
const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
@ -44,12 +45,12 @@ function toBase62(n: number): string {
export function getConfig(): UserConfig {
return {
base: '/vite/',
server: {
port: 5173,
},
plugins: [
compression({ algorithm: 'brotliCompress'}),
pluginVue({
reactivityTransform: true,
}),

View file

@ -980,6 +980,9 @@ importers:
summaly:
specifier: github:misskey-dev/summaly
version: github.com/misskey-dev/summaly/d2d8db49943ccb201c1b1b283e9d0a630519fac7
vite-plugin-compression2:
specifier: ^0.10.4
version: 0.10.4(rollup@3.29.1)
vite-plugin-turbosnap:
specifier: 1.0.3
version: 1.0.3
@ -19126,6 +19129,14 @@ packages:
- terser
dev: true
/vite-plugin-compression2@0.10.4(rollup@3.29.1):
resolution: {integrity: sha512-9YcESw0n1j8KxxY1NJKEcItlT0bLS+K/NKa/xPqZGEHW/qwgigIeRF/bCTUdZ/bn/mg2+PeERWgRmK8G1L0tyg==}
dependencies:
'@rollup/pluginutils': 5.0.4(rollup@3.29.1)
transitivePeerDependencies:
- rollup
dev: true
/vite-plugin-turbosnap@1.0.3:
resolution: {integrity: sha512-p4D8CFVhZS412SyQX125qxyzOgIFouwOcvjZWk6bQbNPR1wtaEzFT6jZxAjf1dejlGqa6fqHcuCvQea6EWUkUA==}
dev: true