Merge branch 'develop' into pr/16456

This commit is contained in:
syuilo 2025-09-17 20:00:21 +09:00
commit ae452dbb54
14 changed files with 362 additions and 94 deletions

View File

@ -4,6 +4,8 @@
- Enhance: 広告ごとにセンシティブフラグを設定できるようになりました
### Client
- Enhance: チャットの日本語名称がダイレクトメッセージに戻るとともに、ベータ版機能ではなくなりました
- Enhance: 画像編集にマスクエフェクトを追加
- Enhance: 時刻計算のための基準値を一か所で管理するようにし、パフォーマンスを向上
### Server

104
locales/index.d.ts vendored
View File

@ -1227,7 +1227,7 @@ export interface Locale extends ILocale {
*/
"noMoreHistory": string;
/**
*
*
*/
"startChat": string;
/**
@ -1927,7 +1927,7 @@ export interface Locale extends ILocale {
*/
"markAsReadAllUnreadNotes": string;
/**
*
*
*/
"markAsReadAllTalkMessages": string;
/**
@ -5390,6 +5390,14 @@ export interface Locale extends ILocale {
*
*/
"chat": string;
/**
*
*/
"directMessage": string;
/**
*
*/
"directMessage_short": string;
/**
*
*/
@ -5529,6 +5537,10 @@ export interface Locale extends ILocale {
*
*/
"thankYouForTestingBeta": string;
/**
*
*/
"createUserSpecifiedNote": string;
"_order": {
/**
*
@ -5540,6 +5552,10 @@ export interface Locale extends ILocale {
"oldest": string;
};
"_chat": {
/**
*
*/
"messages": string;
/**
*
*/
@ -5549,36 +5565,36 @@ export interface Locale extends ILocale {
*/
"newMessage": string;
/**
*
*
*/
"individualChat": string;
/**
*
*
*/
"individualChat_description": string;
/**
*
*
*/
"roomChat": string;
/**
*
*
*
*
*/
"roomChat_description": string;
/**
*
*
*/
"createRoom": string;
/**
*
*
*/
"inviteUserToChat": string;
/**
*
*
*/
"yourRooms": string;
/**
*
*
*/
"joiningRooms": string;
/**
@ -5598,7 +5614,7 @@ export interface Locale extends ILocale {
*/
"noHistory": string;
/**
*
*
*/
"noRooms": string;
/**
@ -5618,7 +5634,7 @@ export interface Locale extends ILocale {
*/
"ignore": string;
/**
* 退
* 退
*/
"leave": string;
/**
@ -5642,35 +5658,35 @@ export interface Locale extends ILocale {
*/
"newline": string;
/**
*
*
*/
"muteThisRoom": string;
/**
*
*
*/
"deleteRoom": string;
/**
*
*
*/
"chatNotAvailableForThisAccountOrServer": string;
/**
*
*
*/
"chatIsReadOnlyForThisAccountOrServer": string;
/**
* 使
* 使
*/
"chatNotAvailableInOtherAccount": string;
/**
*
*
*/
"cannotChatWithTheUser": string;
/**
* 使
* 使
*/
"cannotChatWithTheUser_description": string;
/**
*
*
*/
"youAreNotAMemberOfThisRoomButInvited": string;
/**
@ -5678,31 +5694,31 @@ export interface Locale extends ILocale {
*/
"doYouAcceptInvitation": string;
/**
*
*
*/
"chatWithThisUser": string;
/**
*
*
*/
"thisUserAllowsChatOnlyFromFollowers": string;
/**
*
*
*/
"thisUserAllowsChatOnlyFromFollowing": string;
/**
*
*
*/
"thisUserAllowsChatOnlyFromMutualFollowing": string;
/**
*
*
*/
"thisUserNotAllowedChatAnyone": string;
/**
*
*
*/
"chatAllowedUsers": string;
/**
*
*
*/
"chatAllowedUsers_note": string;
"_chatAllowedUsers": {
@ -7856,7 +7872,7 @@ export interface Locale extends ILocale {
*/
"canImportUserLists": string;
/**
*
*
*/
"chatAvailability": string;
/**
@ -8706,7 +8722,7 @@ export interface Locale extends ILocale {
*/
"badge": string;
/**
*
*
*/
"messageBg": string;
/**
@ -8733,7 +8749,7 @@ export interface Locale extends ILocale {
*/
"reaction": string;
/**
*
*
*/
"chatMessage": string;
};
@ -9017,11 +9033,11 @@ export interface Locale extends ILocale {
*/
"write:following": string;
/**
*
*
*/
"read:messaging": string;
/**
*
*
*/
"write:messaging": string;
/**
@ -9313,11 +9329,11 @@ export interface Locale extends ILocale {
*/
"write:report-abuse": string;
/**
*
*
*/
"write:chat": string;
/**
*
*
*/
"read:chat": string;
};
@ -9543,7 +9559,7 @@ export interface Locale extends ILocale {
*/
"birthdayFollowings": string;
/**
*
*
*/
"chat": string;
};
@ -10283,7 +10299,7 @@ export interface Locale extends ILocale {
*/
"roleAssigned": string;
/**
*
*
*/
"chatRoomInvitationReceived": string;
/**
@ -10396,7 +10412,7 @@ export interface Locale extends ILocale {
*/
"roleAssigned": string;
/**
*
*
*/
"chatRoomInvitationReceived": string;
/**
@ -10578,7 +10594,7 @@ export interface Locale extends ILocale {
*/
"roleTimeline": string;
/**
*
*
*/
"chat": string;
};
@ -10945,7 +10961,7 @@ export interface Locale extends ILocale {
*/
"deleteGalleryPost": string;
/**
*
*
*/
"deleteChatRoom": string;
/**
@ -12362,6 +12378,10 @@ export interface Locale extends ILocale {
*
*/
"tearing": string;
/**
* ()
*/
"fillSquare": string;
};
"_fxProps": {
/**
@ -12376,6 +12396,10 @@ export interface Locale extends ILocale {
*
*/
"size": string;
/**
*
*/
"offset": string;
/**
*
*/

View File

@ -302,7 +302,7 @@ uploadNFiles: "{n}個のファイルをアップロード"
explore: "みつける"
messageRead: "既読"
noMoreHistory: "これより過去の履歴はありません"
startChat: "チャットを始める"
startChat: "メッセージを送る"
nUsersRead: "{n}人が読みました"
agreeTo: "{0}に同意"
agree: "同意する"
@ -477,7 +477,7 @@ notFoundDescription: "指定されたURLに該当するページはありませ
uploadFolder: "既定アップロード先"
markAsReadAllNotifications: "すべての通知を既読にする"
markAsReadAllUnreadNotes: "すべての投稿を既読にする"
markAsReadAllTalkMessages: "すべてのチャットを既読にする"
markAsReadAllTalkMessages: "すべてのダイレクトメッセージを既読にする"
help: "ヘルプ"
inputMessageHere: "ここにメッセージを入力"
close: "閉じる"
@ -1343,6 +1343,8 @@ postForm: "投稿フォーム"
textCount: "文字数"
information: "情報"
chat: "チャット"
directMessage: "ダイレクトメッセージ"
directMessage_short: "メッセージ"
migrateOldSettings: "旧設定情報を移行"
migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。"
compress: "圧縮"
@ -1377,53 +1379,55 @@ pluginsAreDisabledBecauseSafeMode: "セーフモードが有効なため、プ
customCssIsDisabledBecauseSafeMode: "セーフモードが有効なため、カスタムCSSは適用されていません。"
themeIsDefaultBecauseSafeMode: "セーフモードが有効な間はデフォルトのテーマが使用されます。セーフモードをオフにすると元に戻ります。"
thankYouForTestingBeta: "ベータ版の検証にご協力いただきありがとうございます!"
createUserSpecifiedNote: "ユーザー指定ノートを作成"
_order:
newest: "新しい順"
oldest: "古い順"
_chat:
messages: "メッセージ"
noMessagesYet: "まだメッセージはありません"
newMessage: "新しいメッセージ"
individualChat: "個人チャット"
individualChat_description: "特定ユーザーとの一対一のチャットができます。"
roomChat: "ルームチャット"
roomChat_description: "複数人でのチャットができます。\nまた、個人チャットを許可していないユーザーとでも、相手が受け入れればチャットができます。"
createRoom: "ルームを作成"
inviteUserToChat: "ユーザーを招待してチャットを始めましょう"
yourRooms: "作成したルーム"
joiningRooms: "参加中のルーム"
individualChat: "個"
individualChat_description: "特定ユーザーと個別にメッセージのやりとりができます。"
roomChat: "グループ"
roomChat_description: "複数人でメッセージのやりとりができます。\nまた、個別のメッセージを許可していないユーザーとでも、相手が受け入れればやりとりできます。"
createRoom: "グループを作成"
inviteUserToChat: "ユーザーを招待してメッセージを送信しましょう"
yourRooms: "作成したグループ"
joiningRooms: "参加中のグループ"
invitations: "招待"
noInvitations: "招待はありません"
history: "履歴"
noHistory: "履歴はありません"
noRooms: "ルームはありません"
noRooms: "グループはありません"
inviteUser: "ユーザーを招待"
sentInvitations: "送信した招待"
join: "参加"
ignore: "無視"
leave: "ルームから退出"
leave: "グループから退出"
members: "メンバー"
searchMessages: "メッセージを検索"
home: "ホーム"
send: "送信"
newline: "改行"
muteThisRoom: "このルームをミュート"
deleteRoom: "ルームを削除"
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは有効化されていません。"
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでチャットは読み取り専用となっています。新たに書き込んだり、チャットルームを作成・参加したりすることはできません。"
chatNotAvailableInOtherAccount: "相手のアカウントでチャット機能が使えない状態になっています。"
cannotChatWithTheUser: "このユーザーとのチャットを開始できません"
cannotChatWithTheUser_description: "チャットが使えない状態になっているか、相手がチャットを開放していません。"
youAreNotAMemberOfThisRoomButInvited: "あなたはこのルームの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。"
muteThisRoom: "このグループをミュート"
deleteRoom: "グループを削除"
chatNotAvailableForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは有効化されていません。"
chatIsReadOnlyForThisAccountOrServer: "このサーバー、またはこのアカウントでダイレクトメッセージは読み取り専用となっています。新たに書き込んだり、グループを作成・参加したりすることはできません。"
chatNotAvailableInOtherAccount: "相手のアカウントでダイレクトメッセージが使えない状態になっています。"
cannotChatWithTheUser: "このユーザーとのダイレクトメッセージを開始できません"
cannotChatWithTheUser_description: "ダイレクトメッセージが使えない状態になっているか、相手がダイレクトメッセージを開放していません。"
youAreNotAMemberOfThisRoomButInvited: "あなたはこのグループの参加者ではありませんが、招待が届いています。参加するには、招待を承認してください。"
doYouAcceptInvitation: "招待を承認しますか?"
chatWithThisUser: "チャットする"
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみチャットを受け付けています。"
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみチャットを受け付けています。"
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみチャットを受け付けています。"
thisUserNotAllowedChatAnyone: "このユーザーは誰からもチャットを受け付けていません。"
chatAllowedUsers: "チャットを許可する相手"
chatAllowedUsers_note: "自分からチャットメッセージを送った相手とはこの設定に関わらずチャットが可能です。"
chatWithThisUser: "ダイレクトメッセージ"
thisUserAllowsChatOnlyFromFollowers: "このユーザーはフォロワーからのみメッセージを受け付けています。"
thisUserAllowsChatOnlyFromFollowing: "このユーザーは、このユーザーがフォローしているユーザーからのみメッセージを受け付けています。"
thisUserAllowsChatOnlyFromMutualFollowing: "このユーザーは相互フォローのユーザーからのみメッセージを受け付けています。"
thisUserNotAllowedChatAnyone: "このユーザーは誰からもメッセージを受け付けていません。"
chatAllowedUsers: "メッセージを許可する相手"
chatAllowedUsers_note: "自分からメッセージを送った相手とはこの設定に関わらずメッセージの送受信が可能です。"
_chatAllowedUsers:
everyone: "誰でも"
followers: "自分のフォロワーのみ"
@ -2034,7 +2038,7 @@ _role:
canImportFollowing: "フォローのインポートを許可"
canImportMuting: "ミュートのインポートを許可"
canImportUserLists: "リストのインポートを許可"
chatAvailability: "チャットを許可"
chatAvailability: "ダイレクトメッセージを許可"
uploadableFileTypes: "アップロード可能なファイル種別"
uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)"
uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。"
@ -2281,7 +2285,7 @@ _theme:
buttonHoverBg: "ボタンの背景 (ホバー)"
inputBorder: "入力ボックスの縁取り"
badge: "バッジ"
messageBg: "チャットの背景"
messageBg: "メッセージの背景"
fgHighlighted: "強調された文字"
_sfx:
@ -2289,7 +2293,7 @@ _sfx:
noteMy: "ノート(自分)"
notification: "通知"
reaction: "リアクション選択時"
chatMessage: "チャットのメッセージ"
chatMessage: "ダイレクトメッセージ"
_soundSettings:
driveFile: "ドライブの音声を使用"
@ -2369,8 +2373,8 @@ _permissions:
"write:favorites": "お気に入りを操作する"
"read:following": "フォローの情報を見る"
"write:following": "フォロー・フォロー解除する"
"read:messaging": "チャットを見る"
"write:messaging": "チャットを操作する"
"read:messaging": "ダイレクトメッセージを見る"
"write:messaging": "ダイレクトメッセージを操作する"
"read:mutes": "ミュートを見る"
"write:mutes": "ミュートを操作する"
"write:notes": "ノートを作成・削除する"
@ -2443,8 +2447,8 @@ _permissions:
"read:clip-favorite": "クリップのいいねを見る"
"read:federation": "連合に関する情報を取得する"
"write:report-abuse": "違反を報告する"
"write:chat": "チャットを操作する"
"read:chat": "チャットを閲覧する"
"write:chat": "ダイレクトメッセージを操作する"
"read:chat": "ダイレクトメッセージを閲覧する"
_auth:
shareAccessTitle: "アプリへのアクセス許可"
@ -2507,7 +2511,7 @@ _widgets:
chooseList: "リストを選択"
clicker: "クリッカー"
birthdayFollowings: "今日誕生日のユーザー"
chat: "チャット"
chat: "ダイレクトメッセージ"
_cw:
hide: "隠す"
@ -2714,7 +2718,7 @@ _notification:
newNote: "新しい投稿"
unreadAntennaNote: "アンテナ {name}"
roleAssigned: "ロールが付与されました"
chatRoomInvitationReceived: "チャットルームへ招待されました"
chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待されました"
emptyPushNotificationMessage: "プッシュ通知の更新をしました"
achievementEarned: "実績を獲得"
testNotification: "通知テスト"
@ -2744,7 +2748,7 @@ _notification:
receiveFollowRequest: "フォロー申請を受け取った"
followRequestAccepted: "フォローが受理された"
roleAssigned: "ロールが付与された"
chatRoomInvitationReceived: "チャットルームへ招待された"
chatRoomInvitationReceived: "ダイレクトメッセージのグループへ招待された"
achievementEarned: "実績の獲得"
exportCompleted: "エクスポートが完了した"
login: "ログイン"
@ -2794,7 +2798,7 @@ _deck:
mentions: "メンション"
direct: "指名"
roleTimeline: "ロールタイムライン"
chat: "チャット"
chat: "ダイレクトメッセージ"
_dialog:
charactersExceeded: "最大文字数を超えています! 現在 {current} / 制限 {max}"
@ -2897,7 +2901,7 @@ _moderationLogTypes:
deletePage: "ページを削除"
deleteFlash: "Playを削除"
deleteGalleryPost: "ギャラリーの投稿を削除"
deleteChatRoom: "チャットルームを削除"
deleteChatRoom: "ダイレクトメッセージのグループを削除"
updateProxyAccountDescription: "プロキシアカウントの説明を更新"
_fileViewer:
@ -3310,11 +3314,13 @@ _imageEffector:
checker: "チェッカー"
blockNoise: "ブロックノイズ"
tearing: "ティアリング"
fillSquare: "塗りつぶし(四角)"
_fxProps:
angle: "角度"
scale: "サイズ"
size: "サイズ"
offset: "位置"
color: "色"
opacity: "不透明度"
normalize: "正規化"

View File

@ -109,6 +109,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
chatAvailability: 'available',
uploadableFileTypes: [
'text/plain',
'text/csv',
'application/json',
'image/*',
'video/*',

View File

@ -19,9 +19,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root">
<div :class="$style.container">
<div :class="$style.preview">
<canvas ref="canvasEl" :class="$style.previewCanvas"></canvas>
<canvas ref="canvasEl" :class="$style.previewCanvas" @pointerdown="onImagePointerdown"></canvas>
<div :class="$style.previewContainer">
<div class="_acrylic" :class="$style.previewTitle">{{ i18n.ts.preview }}</div>
<div class="_acrylic" :class="$style.editControls">
<button class="_button" :class="[$style.previewControlsButton, fillSquare ? $style.active : null]" @click="fillSquare = true"><i class="ti ti-pencil"></i></button>
</div>
<div class="_acrylic" :class="$style.previewControls">
<button class="_button" :class="[$style.previewControlsButton, !enabled ? $style.active : null]" @click="enabled = false">Before</button>
<button class="_button" :class="[$style.previewControlsButton, enabled ? $style.active : null]" @click="enabled = true">After</button>
@ -212,6 +215,100 @@ watch(enabled, () => {
renderer.render();
}
});
const fillSquare = ref(false);
function onImagePointerdown(ev: PointerEvent) {
if (canvasEl.value == null || imageBitmap == null || !fillSquare.value) return;
const AW = canvasEl.value.clientWidth;
const AH = canvasEl.value.clientHeight;
const BW = imageBitmap.width;
const BH = imageBitmap.height;
let xOffset = 0;
let yOffset = 0;
if (AW / AH < BW / BH) { //
yOffset = AH - BH * (AW / BW);
} else { //
xOffset = AW - BW * (AH / BH);
}
xOffset /= 2;
yOffset /= 2;
let startX = ev.offsetX - xOffset;
let startY = ev.offsetY - yOffset;
if (AW / AH < BW / BH) { //
startX = startX / (Math.max(AW, AH) / Math.max(BH / BW, 1));
startY = startY / (Math.max(AW, AH) / Math.max(BW / BH, 1));
} else { //
startX = startX / (Math.min(AW, AH) / Math.max(BH / BW, 1));
startY = startY / (Math.min(AW, AH) / Math.max(BW / BH, 1));
}
const id = genId();
layers.push({
id,
fxId: 'fillSquare',
params: {
offsetX: 0,
offsetY: 0,
scaleX: 0.1,
scaleY: 0.1,
angle: 0,
opacity: 1,
color: [1, 1, 1],
},
});
_move(ev.offsetX, ev.offsetY);
function _move(pointerX: number, pointerY: number) {
let x = pointerX - xOffset;
let y = pointerY - yOffset;
if (AW / AH < BW / BH) { //
x = x / (Math.max(AW, AH) / Math.max(BH / BW, 1));
y = y / (Math.max(AW, AH) / Math.max(BW / BH, 1));
} else { //
x = x / (Math.min(AW, AH) / Math.max(BH / BW, 1));
y = y / (Math.min(AW, AH) / Math.max(BW / BH, 1));
}
const scaleX = Math.abs(x - startX);
const scaleY = Math.abs(y - startY);
const layerIndex = layers.findIndex((l) => l.id === id);
const layer = layerIndex !== -1 ? layers[layerIndex] : null;
if (layer != null) {
layer.params.offsetX = (x + startX) - 1;
layer.params.offsetY = (y + startY) - 1;
layer.params.scaleX = scaleX;
layer.params.scaleY = scaleY;
layers[layerIndex] = layer;
}
}
function move(ev: PointerEvent) {
_move(ev.offsetX, ev.offsetY);
}
function up() {
canvasEl.value?.removeEventListener('pointermove', move);
canvasEl.value?.removeEventListener('pointerup', up);
canvasEl.value?.removeEventListener('pointercancel', up);
canvasEl.value?.releasePointerCapture(ev.pointerId);
fillSquare.value = false;
}
canvasEl.value.addEventListener('pointermove', move);
canvasEl.value.addEventListener('pointerup', up);
canvasEl.value.setPointerCapture(ev.pointerId);
}
</script>
<style module>
@ -251,6 +348,18 @@ watch(enabled, () => {
font-size: 85%;
}
.editControls {
position: absolute;
z-index: 100;
bottom: 8px;
left: 8px;
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 6px;
}
.previewControls {
position: absolute;
z-index: 100;
@ -283,9 +392,11 @@ watch(enabled, () => {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 20px;
width: -webkit-fill-available;
width: stretch;
height: -webkit-fill-available;
height: stretch;
margin: 20px;
box-sizing: border-box;
object-fit: contain;
}

View File

@ -117,7 +117,7 @@ export const navbarItemDef = reactive({
to: '/channels',
},
chat: {
title: i18n.ts.chat,
title: i18n.ts.directMessage_short,
icon: 'ti ti-messages',
to: '/chat',
show: computed(() => $i != null && $i.policies.chatAvailability !== 'unavailable'),

View File

@ -48,7 +48,7 @@ const headerTabs = computed(() => [{
}]);
definePage(() => ({
title: i18n.ts.chat + ' (beta)',
title: i18n.ts.directMessage,
icon: 'ti ti-messages',
}));
</script>

View File

@ -46,6 +46,6 @@ onMounted(() => {
});
definePage({
title: i18n.ts.chat,
title: i18n.ts.directMessage,
});
</script>

View File

@ -421,7 +421,7 @@ const tab = ref('chat');
const headerTabs = computed(() => room.value ? [{
key: 'chat',
title: i18n.ts.chat,
title: i18n.ts._chat.messages,
icon: 'ti ti-messages',
}, {
key: 'members',
@ -437,7 +437,7 @@ const headerTabs = computed(() => room.value ? [{
icon: 'ti ti-info-circle',
}] : [{
key: 'chat',
title: i18n.ts.chat,
title: i18n.ts._chat.messages,
icon: 'ti ti-messages',
}, {
key: 'search',
@ -466,12 +466,12 @@ definePage(computed(() => {
};
} else {
return {
title: i18n.ts.chat,
title: i18n.ts.directMessage,
};
}
} else {
return {
title: i18n.ts.chat,
title: i18n.ts.directMessage,
};
}
}));

View File

@ -414,7 +414,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="$i.policies.chatAvailability !== 'unavailable'">
<SearchMarker v-slot="slotProps" :keywords="['chat', 'messaging']">
<MkFolder :defaultOpen="slotProps.isParentOfTarget">
<template #label><SearchLabel>{{ i18n.ts.chat }}</SearchLabel></template>
<template #label><SearchLabel>{{ i18n.ts.directMessage }}</SearchLabel></template>
<template #icon><SearchIcon><i class="ti ti-messages"></i></SearchIcon></template>
<div class="_gaps_s">

View File

@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<SearchMarker :keywords="['chat']">
<FormSection>
<template #label><SearchLabel>{{ i18n.ts.chat }}</SearchLabel></template>
<template #label><SearchLabel>{{ i18n.ts.directMessage }}</SearchLabel></template>
<div class="_gaps_m">
<MkInfo v-if="$i.policies.chatAvailability === 'unavailable'">{{ i18n.ts._chat.chatNotAvailableForThisAccountOrServer }}</MkInfo>

View File

@ -376,8 +376,8 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router
//}
menuItems.push({ type: 'divider' }, {
icon: 'ti ti-mail',
text: i18n.ts.sendMessage,
icon: 'ti ti-pencil-heart',
text: i18n.ts.createUserSpecifiedNote,
action: () => {
const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${user.host}`;
os.post({ specified: user, initialText: `${canonical} ` });

View File

@ -18,6 +18,7 @@ import { FX_stripe } from './fxs/stripe.js';
import { FX_threshold } from './fxs/threshold.js';
import { FX_zoomLines } from './fxs/zoomLines.js';
import { FX_blockNoise } from './fxs/blockNoise.js';
import { FX_fillSquare } from './fxs/fillSquare.js';
import type { ImageEffectorFx } from './ImageEffector.js';
export const FXS = [
@ -36,4 +37,5 @@ export const FXS = [
FX_chromaticAberration,
FX_tearing,
FX_blockNoise,
FX_fillSquare,
] as const satisfies ImageEffectorFx<string, any>[];

View File

@ -0,0 +1,122 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { defineImageEffectorFx } from '../ImageEffector.js';
import { i18n } from '@/i18n.js';
const shader = `#version 300 es
precision mediump float;
const float PI = 3.141592653589793;
const float TWO_PI = 6.283185307179586;
const float HALF_PI = 1.5707963267948966;
in vec2 in_uv;
uniform sampler2D in_texture;
uniform vec2 in_resolution;
uniform vec2 u_offset;
uniform vec2 u_scale;
uniform float u_angle;
uniform vec3 u_color;
uniform float u_opacity;
out vec4 out_color;
void main() {
vec4 in_color = texture(in_texture, in_uv);
//float x_ratio = max(in_resolution.x / in_resolution.y, 1.0);
//float y_ratio = max(in_resolution.y / in_resolution.x, 1.0);
float angle = -(u_angle * PI);
vec2 centeredUv = in_uv - vec2(0.5, 0.5) - u_offset;
vec2 rotatedUV = vec2(
centeredUv.x * cos(angle) - centeredUv.y * sin(angle),
centeredUv.x * sin(angle) + centeredUv.y * cos(angle)
) + u_offset;
bool isInside = rotatedUV.x > u_offset.x - u_scale.x && rotatedUV.x < u_offset.x + u_scale.x && rotatedUV.y > u_offset.y - u_scale.y && rotatedUV.y < u_offset.y + u_scale.y;
out_color = isInside ? vec4(
mix(in_color.r, u_color.r, u_opacity),
mix(in_color.g, u_color.g, u_opacity),
mix(in_color.b, u_color.b, u_opacity),
in_color.a
) : in_color;
}
`;
export const FX_fillSquare = defineImageEffectorFx({
id: 'fillSquare',
name: i18n.ts._imageEffector._fxs.fillSquare,
shader,
uniforms: ['offset', 'scale', 'angle', 'color', 'opacity'] as const,
params: {
offsetX: {
label: i18n.ts._imageEffector._fxProps.offset + ' X',
type: 'number',
default: 0.0,
min: -1.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 100) + '%',
},
offsetY: {
label: i18n.ts._imageEffector._fxProps.offset + ' Y',
type: 'number',
default: 0.0,
min: -1.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 100) + '%',
},
scaleX: {
label: i18n.ts._imageEffector._fxProps.scale + ' X',
type: 'number',
default: 0.5,
min: 0.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 100) + '%',
},
scaleY: {
label: i18n.ts._imageEffector._fxProps.scale + ' Y',
type: 'number',
default: 0.5,
min: 0.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 100) + '%',
},
angle: {
label: i18n.ts._imageEffector._fxProps.angle,
type: 'number',
default: 0,
min: -1.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 90) + '°',
},
color: {
label: i18n.ts._imageEffector._fxProps.color,
type: 'color',
default: [1, 1, 1],
},
opacity: {
label: i18n.ts._imageEffector._fxProps.opacity,
type: 'number',
default: 1.0,
min: 0.0,
max: 1.0,
step: 0.01,
toViewValue: v => Math.round(v * 100) + '%',
},
},
main: ({ gl, u, params }) => {
gl.uniform2f(u.offset, params.offsetX / 2, params.offsetY / 2);
gl.uniform2f(u.scale, params.scaleX / 2, params.scaleY / 2);
gl.uniform1f(u.angle, params.angle / 2);
gl.uniform3f(u.color, params.color[0], params.color[1], params.color[2]);
gl.uniform1f(u.opacity, params.opacity);
},
});