Merge branch 'develop' into feat-14852

This commit is contained in:
かっこかり 2024-11-09 15:50:56 +09:00 committed by GitHub
commit d6b3dc80b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 219 additions and 69 deletions

View File

@ -16,6 +16,7 @@
- 認証するアカウントを切り替えられるように - 認証するアカウントを切り替えられるように
- Enhance: Self-XSS防止用の警告を追加 - Enhance: Self-XSS防止用の警告を追加
- Enhance: カタルーニャ語 (ca-ES) に対応 - Enhance: カタルーニャ語 (ca-ES) に対応
- Enhance: 個別お知らせページではMetaタグを出力するように
- Enhance: 過去に送信したフォローリクエストを確認できるように - Enhance: 過去に送信したフォローリクエストを確認できるように
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/663) (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/663)
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正 - Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
@ -24,15 +25,25 @@
- Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正 - Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正
- Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used - Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used
- Fix: リンク切れを修正 - Fix: リンク切れを修正
= Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正
(Cherry-picked from https://github.com/taiyme/misskey/pull/305)
### Server ### Server
- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように - Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588) (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588)
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715)
- fix(backend): フォロワーへのメッセージの絵文字をemojisに含めるように
- Fix: Nested proxy requestsを検出した際にブロックするように - Fix: Nested proxy requestsを検出した際にブロックするように
[ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236) [ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236)
- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正 - Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706)
- Fix: 連合への配信時に、acctの大小文字が区別されてしまい正しくメンションが処理されないことがある問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/711)
- Fix: ローカルユーザーへのメンションを含むートが連合される際に正しいURLに変換されないことがある問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/712)
- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
- Enhance: リモートユーザーの照会をオリジナルにリダイレクトするように
### Misskey.js ### Misskey.js
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正 - Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正

View File

@ -83,6 +83,10 @@ One should not add property that has defined before by other implementation, or
## Reviewers guide ## Reviewers guide
Be willing to comment on the good points and not just the things you want fixed 💯 Be willing to comment on the good points and not just the things you want fixed 💯
読んでおくといいやつ
- https://blog.lacolaco.net/posts/1e2cf439b3c2/
- https://konifar-zatsu.hatenadiary.jp/entry/2024/11/05/192421
### Review perspective ### Review perspective
- Scope - Scope
- Are the goals of the PR clear? - Are the goals of the PR clear?

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2024.10.2-alpha.2", "version": "2024.11.0-alpha.0",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -406,8 +406,10 @@ export class MfmService {
mention: (node) => { mention: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
const { username, host, acct } = node.props; const { username, host, acct } = node.props;
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`); a.setAttribute('href', remoteUserInfo
? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri)
: `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`);
a.className = 'u-url mention'; a.className = 'u-url mention';
a.textContent = acct; a.textContent = acct;
return a; return a;

View File

@ -4,5 +4,5 @@
*/ */
export function sqlLikeEscape(s: string) { export function sqlLikeEscape(s: string) {
return s.replace(/([%_])/g, '\\$1'); return s.replace(/([\\%_])/g, '\\$1');
} }

View File

@ -29,6 +29,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IActivity } from '@/core/activitypub/type.js'; import { IActivity } from '@/core/activitypub/type.js';
import { isQuote, isRenote } from '@/misc/is-renote.js'; import { isQuote, isRenote } from '@/misc/is-renote.js';
import * as Acct from '@/misc/acct.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import type { FindOptionsWhere } from 'typeorm'; import type { FindOptionsWhere } from 'typeorm';
@ -486,6 +487,16 @@ export class ActivityPubServerService {
return; return;
} }
// リモートだったらリダイレクト
if (user.host != null) {
if (user.uri == null || this.utilityService.isSelfHost(user.host)) {
reply.code(500);
return;
}
reply.redirect(user.uri, 301);
return;
}
reply.header('Cache-Control', 'public, max-age=180'); reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply); this.setResponseType(request, reply);
return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser))); return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser)));
@ -654,19 +665,20 @@ export class ActivityPubServerService {
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
id: userId, id: userId,
host: IsNull(),
isSuspended: false, isSuspended: false,
}); });
return await this.userInfo(request, reply, user); return await this.userInfo(request, reply, user);
}); });
fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { fastify.get<{ Params: { acct: string; } }>('/@:acct', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
vary(reply.raw, 'Accept'); vary(reply.raw, 'Accept');
const acct = Acct.parse(request.params.acct);
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
usernameLower: request.params.user.toLowerCase(), usernameLower: acct.username,
host: IsNull(), host: acct.host ?? IsNull(),
isSuspended: false, isSuspended: false,
}); });

View File

@ -465,6 +465,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const newName = updates.name === undefined ? user.name : updates.name; const newName = updates.name === undefined ? user.name : updates.name;
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields; const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
const newFollowedMessage = profileUpdates.followedMessage === undefined ? profile.followedMessage : profileUpdates.followedMessage;
if (newName != null) { if (newName != null) {
let hasProhibitedWords = false; let hasProhibitedWords = false;
@ -494,6 +495,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
]); ]);
} }
if (newFollowedMessage != null) {
const tokens = mfm.parse(newFollowedMessage);
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
}
updates.emojis = emojis; updates.emojis = emojis;
updates.tags = tags; updates.tags = tags;

View File

@ -112,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.activeUsersChart.read(me); this.activeUsersChart.read(me);
await this.noteEntityService.packMany(timeline, me); return await this.noteEntityService.packMany(timeline, me);
} }
const timeline = await this.fanoutTimelineEndpointService.timeline({ const timeline = await this.fanoutTimelineEndpointService.timeline({

View File

@ -42,13 +42,26 @@ import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type {
AnnouncementsRepository,
ChannelsRepository,
ClipsRepository,
FlashsRepository,
GalleryPostsRepository,
MiMeta,
NotesRepository,
PagesRepository,
ReversiGamesRepository,
UserProfilesRepository,
UsersRepository,
} from '@/models/_.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { FeedService } from './FeedService.js'; import { FeedService } from './FeedService.js';
import { UrlPreviewService } from './UrlPreviewService.js'; import { UrlPreviewService } from './UrlPreviewService.js';
import { ClientLoggerService } from './ClientLoggerService.js'; import { ClientLoggerService } from './ClientLoggerService.js';
@ -103,6 +116,9 @@ export class ClientServerService {
@Inject(DI.reversiGamesRepository) @Inject(DI.reversiGamesRepository)
private reversiGamesRepository: ReversiGamesRepository, private reversiGamesRepository: ReversiGamesRepository,
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
private flashEntityService: FlashEntityService, private flashEntityService: FlashEntityService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
@ -112,6 +128,7 @@ export class ClientServerService {
private clipEntityService: ClipEntityService, private clipEntityService: ClipEntityService,
private channelEntityService: ChannelEntityService, private channelEntityService: ChannelEntityService,
private reversiGameEntityService: ReversiGameEntityService, private reversiGameEntityService: ReversiGameEntityService,
private announcementEntityService: AnnouncementEntityService,
private urlPreviewService: UrlPreviewService, private urlPreviewService: UrlPreviewService,
private feedService: FeedService, private feedService: FeedService,
private roleService: RoleService, private roleService: RoleService,
@ -776,6 +793,24 @@ export class ClientServerService {
return await renderBase(reply); return await renderBase(reply);
} }
}); });
// 個別お知らせページ
fastify.get<{ Params: { announcementId: string; } }>('/announcements/:announcementId', async (request, reply) => {
const announcement = await this.announcementsRepository.findOneBy({
id: request.params.announcementId,
});
if (announcement) {
const _announcement = await this.announcementEntityService.pack(announcement);
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('announcement', {
announcement: _announcement,
...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
}
});
//#endregion //#endregion
//#region noindex pages //#region noindex pages

View File

@ -0,0 +1,21 @@
extends ./base
block vars
- const title = announcement.title;
- const description = announcement.text.length > 100 ? announcement.text.slice(0, 100) + '…' : announcement.text;
- const url = `${config.url}/announcements/${announcement.id}`;
block title
= `${title} | ${instanceName}`
block desc
meta(name='description' content=description)
block og
meta(property='og:type' content='article')
meta(property='og:title' content= title)
meta(property='og:description' content= description)
meta(property='og:url' content= url)
if announcement.imageUrl
meta(property='og:image' content=announcement.imageUrl)
meta(property='twitter:card' content='summary_large_image')

View File

@ -2,6 +2,7 @@ block vars
block loadClientEntry block loadClientEntry
- const entry = config.frontendEntry; - const entry = config.frontendEntry;
- const baseUrl = config.url;
doctype html doctype html
@ -32,7 +33,7 @@ html
link(rel='icon' href= icon || '/favicon.ico') link(rel='icon' href= icon || '/favicon.ico')
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png') link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
link(rel='manifest' href='/manifest.json') link(rel='manifest' href='/manifest.json')
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`) link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${baseUrl}/opensearch.xml`)
link(rel='prefetch' href=serverErrorImageUrl) link(rel='prefetch' href=serverErrorImageUrl)
link(rel='prefetch' href=infoImageUrl) link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl) link(rel='prefetch' href=notFoundImageUrl)

View File

@ -230,6 +230,7 @@ describe('Webリソース', () => {
path: path('xxxxxxxxxx'), path: path('xxxxxxxxxx'),
type: HTML, type: HTML,
})); }));
test.todo('HTMLとしてGETできる。(リモートユーザーでもリダイレクトせず)');
}); });
describe.each([ describe.each([
@ -249,6 +250,7 @@ describe('Webリソース', () => {
path: path('xxxxxxxxxx'), path: path('xxxxxxxxxx'),
accept, accept,
})); }));
test.todo('はオリジナルにリダイレクトされる。(リモートユーザー)');
}); });
}); });

View File

@ -15,7 +15,7 @@ import { updateI18n, i18n } from '@/i18n.js';
import { $i, refreshAccount, login } from '@/account.js'; import { $i, refreshAccount, login } from '@/account.js';
import { defaultStore, ColdDeviceStorage } from '@/store.js'; import { defaultStore, ColdDeviceStorage } from '@/store.js';
import { fetchInstance, instance } from '@/instance.js'; import { fetchInstance, instance } from '@/instance.js';
import { deviceKind } from '@/scripts/device-kind.js'; import { deviceKind, updateDeviceKind } from '@/scripts/device-kind.js';
import { reloadChannel } from '@/scripts/unison-reload.js'; import { reloadChannel } from '@/scripts/unison-reload.js';
import { getUrlWithoutLoginId } from '@/scripts/login-id.js'; import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
import { getAccountFromId } from '@/scripts/get-account-from-id.js'; import { getAccountFromId } from '@/scripts/get-account-from-id.js';
@ -185,6 +185,10 @@ export async function common(createVue: () => App<Element>) {
} }
}); });
watch(defaultStore.reactiveState.overridedDeviceKind, (kind) => {
updateDeviceKind(kind);
}, { immediate: true });
watch(defaultStore.reactiveState.useBlurEffectForModal, v => { watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none'); document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
}, { immediate: true }); }, { immediate: true });

View File

@ -118,7 +118,7 @@ import { hms } from '@/filters/hms.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { isFullscreenNotSupported } from '@/scripts/device-kind.js'; import { exitFullscreen, requestFullscreen } from '@/scripts/fullscreen.js';
import hasAudio from '@/scripts/media-has-audio.js'; import hasAudio from '@/scripts/media-has-audio.js';
import MkMediaRange from '@/components/MkMediaRange.vue'; import MkMediaRange from '@/components/MkMediaRange.vue';
import { $i, iAmModerator } from '@/account.js'; import { $i, iAmModerator } from '@/account.js';
@ -334,27 +334,22 @@ function togglePlayPause() {
} }
function toggleFullscreen() { function toggleFullscreen() {
if (isFullscreenNotSupported && videoEl.value) { if (playerEl.value == null || videoEl.value == null) return;
if (isFullscreen.value) { if (isFullscreen.value) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment exitFullscreen({
//@ts-ignore videoEl: videoEl.value,
videoEl.value.webkitExitFullscreen(); });
isFullscreen.value = false; isFullscreen.value = false;
} else { } else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment requestFullscreen({
//@ts-ignore videoEl: videoEl.value,
videoEl.value.webkitEnterFullscreen(); playerEl: playerEl.value,
options: {
navigationUI: 'hide',
},
});
isFullscreen.value = true; isFullscreen.value = true;
} }
} else if (playerEl.value) {
if (isFullscreen.value) {
document.exitFullscreen();
isFullscreen.value = false;
} else {
playerEl.value.requestFullscreen({ navigationUI: 'hide' });
isFullscreen.value = true;
}
}
} }
function togglePictureInPicture() { function togglePictureInPicture() {
@ -454,8 +449,10 @@ watch(loop, (to) => {
}); });
watch(hide, (to) => { watch(hide, (to) => {
if (to && isFullscreen.value) { if (videoEl.value && to && isFullscreen.value) {
document.exitFullscreen(); exitFullscreen({
videoEl: videoEl.value,
});
isFullscreen.value = false; isFullscreen.value = false;
} }
}); });

View File

@ -292,15 +292,18 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute'; function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
*/ */
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' { function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' {
if (mutedWords == null) return false; if (mutedWords != null) {
if (checkWordMute(noteToCheck, $i, mutedWords)) return true; if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true; if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true; if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
}
if (checkOnly) return false; if (checkOnly) return false;
if (inTimeline && !tl_withSensitive.value && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute'; if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
return 'sensitiveMute';
}
return false; return false;
} }

View File

@ -1108,7 +1108,7 @@ defineExpose({
&:focus-visible { &:focus-visible {
outline: none; outline: none;
.submitInner { > .submitInner {
outline: 2px solid var(--MI_THEME-fgOnAccent); outline: 2px solid var(--MI_THEME-fgOnAccent);
outline-offset: -4px; outline-offset: -4px;
} }
@ -1123,13 +1123,13 @@ defineExpose({
} }
&:not(:disabled):hover { &:not(:disabled):hover {
> .inner { > .submitInner {
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5))); background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
} }
} }
&:not(:disabled):active { &:not(:disabled):active {
> .inner { > .submitInner {
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5))); background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
} }
} }

View File

@ -43,6 +43,7 @@ const props = withDefaults(defineProps<{
}>(), { }>(), {
withRenotes: true, withRenotes: true,
withReplies: false, withReplies: false,
withSensitive: true,
onlyFiles: false, onlyFiles: false,
}); });

View File

@ -103,7 +103,7 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []); const headerTabs = computed(() => []);
definePageMetadata(() => ({ definePageMetadata(() => ({
title: announcement.value ? `${i18n.ts.announcements}: ${announcement.value.title}` : i18n.ts.announcements, title: announcement.value ? announcement.value.title : i18n.ts.announcements,
icon: 'ti ti-speakerphone', icon: 'ti ti-speakerphone',
})); }));
</script> </script>

View File

@ -62,7 +62,7 @@ function accepted() {
state.value = 'accepted'; state.value = 'accepted';
if (session.value && session.value.app.callbackUrl) { if (session.value && session.value.app.callbackUrl) {
const url = new URL(session.value.app.callbackUrl); const url = new URL(session.value.app.callbackUrl);
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url'); if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(url.protocol)) throw new Error('invalid url');
location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`; location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`;
} }
} }

View File

@ -65,7 +65,7 @@ async function onAccept(token: string) {
if (props.callback && props.callback !== '') { if (props.callback && props.callback !== '') {
const cbUrl = new URL(props.callback); const cbUrl = new URL(props.callback);
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url'); if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(cbUrl.protocol)) throw new Error('invalid url');
cbUrl.searchParams.set('session', props.session); cbUrl.searchParams.set('session', props.session);
location.href = cbUrl.toString(); location.href = cbUrl.toString();
} else { } else {

View File

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.tl"> <div :class="$style.tl">
<MkTimeline <MkTimeline
ref="tlComponent" ref="tlComponent"
:key="src + withRenotes + withReplies + onlyFiles" :key="src + withRenotes + withReplies + onlyFiles + withSensitive"
:src="src.split(':')[0]" :src="src.split(':')[0]"
:list="src.split(':')[1]" :list="src.split(':')[1]"
:withRenotes="withRenotes" :withRenotes="withRenotes"

View File

@ -241,9 +241,13 @@ export class Storage<T extends StateDef> {
* getter/setterを作ります * getter/setterを作ります
* vue上で設定コントロールのmodelとして使う用 * vue上で設定コントロールのmodelとして使う用
*/ */
public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]): { public makeGetterSetter<K extends keyof T, R = T[K]['default']>(
get: () => T[K]['default']; key: K,
set: (value: T[K]['default']) => void; getter?: (v: T[K]['default']) => R,
setter?: (v: R) => T[K]['default'],
): {
get: () => R;
set: (value: R) => void;
} { } {
const valueRef = ref(this.state[key]); const valueRef = ref(this.state[key]);
@ -265,7 +269,7 @@ export class Storage<T extends StateDef> {
return valueRef.value; return valueRef.value;
} }
}, },
set: (value: unknown) => { set: (value) => {
const val = setter ? setter(value) : value; const val = setter ? setter(value) : value;
this.set(key, val); this.set(key, val);
valueRef.value = val; valueRef.value = val;

View File

@ -3,22 +3,22 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { defaultStore } from '@/store.js'; export type DeviceKind = 'smartphone' | 'tablet' | 'desktop';
await defaultStore.ready;
const ua = navigator.userAgent.toLowerCase(); const ua = navigator.userAgent.toLowerCase();
const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700); const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700);
const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua); const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua);
const isIPhone = /iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1; export const DEFAULT_DEVICE_KIND: DeviceKind = (
// navigator.platform may be deprecated but this check is still required isSmartphone
const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1; ? 'smartphone'
const isIos = /ipad|iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1; : isTablet
? 'tablet'
: 'desktop'
);
export const isFullscreenNotSupported = isIPhone || isIos; export let deviceKind: DeviceKind = DEFAULT_DEVICE_KIND;
export const deviceKind: 'smartphone' | 'tablet' | 'desktop' = defaultStore.state.overridedDeviceKind ? defaultStore.state.overridedDeviceKind export function updateDeviceKind(kind: DeviceKind | null) {
: isSmartphone ? 'smartphone' deviceKind = kind ?? DEFAULT_DEVICE_KIND;
: isTablet ? 'tablet' }
: 'desktop';

View File

@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
type PartiallyPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type VideoEl = PartiallyPartial<HTMLVideoElement, 'requestFullscreen'> & {
webkitEnterFullscreen?(): void;
webkitExitFullscreen?(): void;
};
type PlayerEl = PartiallyPartial<HTMLElement, 'requestFullscreen'>;
type RequestFullscreenProps = {
readonly videoEl: VideoEl;
readonly playerEl: PlayerEl;
readonly options?: FullscreenOptions | null;
};
type ExitFullscreenProps = {
readonly videoEl: VideoEl;
};
export const requestFullscreen = ({ videoEl, playerEl, options }: RequestFullscreenProps) => {
if (playerEl.requestFullscreen != null) {
playerEl.requestFullscreen(options ?? undefined);
return;
}
if (videoEl.webkitEnterFullscreen != null) {
videoEl.webkitEnterFullscreen();
return;
}
};
export const exitFullscreen = ({ videoEl }: ExitFullscreenProps) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (document.exitFullscreen != null) {
document.exitFullscreen();
return;
}
if (videoEl.webkitExitFullscreen != null) {
videoEl.webkitExitFullscreen();
return;
}
};

View File

@ -8,8 +8,9 @@ import * as Misskey from 'misskey-js';
import { hemisphere } from '@@/js/intl-const.js'; import { hemisphere } from '@@/js/intl-const.js';
import lightTheme from '@@/themes/l-light.json5'; import lightTheme from '@@/themes/l-light.json5';
import darkTheme from '@@/themes/d-green-lime.json5'; import darkTheme from '@@/themes/d-green-lime.json5';
import { miLocalStorage } from './local-storage.js';
import type { SoundType } from '@/scripts/sound.js'; import type { SoundType } from '@/scripts/sound.js';
import { DEFAULT_DEVICE_KIND, type DeviceKind } from '@/scripts/device-kind.js';
import { miLocalStorage } from '@/local-storage.js';
import { Storage } from '@/pizzax.js'; import { Storage } from '@/pizzax.js';
import type { Ast } from '@syuilo/aiscript'; import type { Ast } from '@syuilo/aiscript';
@ -207,7 +208,7 @@ export const defaultStore = markRaw(new Storage('base', {
overridedDeviceKind: { overridedDeviceKind: {
where: 'device', where: 'device',
default: null as null | 'smartphone' | 'tablet' | 'desktop', default: null as DeviceKind | null,
}, },
serverDisconnectedBehavior: { serverDisconnectedBehavior: {
where: 'device', where: 'device',
@ -263,11 +264,11 @@ export const defaultStore = markRaw(new Storage('base', {
}, },
useBlurEffectForModal: { useBlurEffectForModal: {
where: 'device', where: 'device',
default: !/mobile|iphone|android/.test(navigator.userAgent.toLowerCase()), // 循環参照するのでdevice-kind.tsは参照できない default: DEFAULT_DEVICE_KIND === 'desktop',
}, },
useBlurEffect: { useBlurEffect: {
where: 'device', where: 'device',
default: !/mobile|iphone|android/.test(navigator.userAgent.toLowerCase()), // 循環参照するのでdevice-kind.tsは参照できない default: DEFAULT_DEVICE_KIND === 'desktop',
}, },
showFixedPostForm: { showFixedPostForm: {
where: 'device', where: 'device',

View File

@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "misskey-js", "name": "misskey-js",
"version": "2024.10.2-alpha.2", "version": "2024.11.0-alpha.0",
"description": "Misskey SDK for JavaScript", "description": "Misskey SDK for JavaScript",
"license": "MIT", "license": "MIT",
"main": "./built/index.js", "main": "./built/index.js",