Feat: emoji picker profile

This commit is contained in:
mattyatea 2024-01-14 13:54:37 +09:00
parent 693efb99a0
commit 6fa319afc6
12 changed files with 502 additions and 345 deletions

View File

@ -118,7 +118,7 @@ redis:
# ┌───────────────────────────┐
#───┘ MeiliSearch configuration └─────────────────────────────
# You can set scope to local (default value) or global
# You can set scope to local (default value) or global
# (include notes from remote).
#meilisearch:
@ -180,7 +180,9 @@ id: 'aidx'
#outgoingAddressFamily: ipv4
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128
#
proxyBypassHosts:
- api.deepl.com
@ -214,7 +216,7 @@ proxyRemoteFiles: true
signToActivityPubGet: true
# For security reasons, uploading attachments from the intranet is prohibited,
# but exceptions can be made from the following settings. Default value is "undefined".
# but exceptions can be made from the following settings. Default value is "undefined".
# Read changelog to learn more (Improvements of 12.90.0 (2021/09/04)).
#allowedPrivateNetworks: [
# '127.0.0.1/32'

View File

@ -9,6 +9,8 @@ notifications: "Notifications"
username: "Username"
password: "Password"
forgotPassword: "Forgot password"
setDefaultProfileConfirm: "Do you want to make this profile the default?"
emojiPickerProfile: "Emoji picker profile"
fetchingAsApObject: "Fetching from the Fediverse..."
ok: "OK"
gotIt: "Got it!"

3
locales/index.d.ts vendored
View File

@ -14,6 +14,8 @@ export interface Locale {
"forgotPassword": string;
"fetchingAsApObject": string;
"ok": string;
"setDefaultProfileConfirm": string;
"emojiPickerProfile": string;
"notificationIndicator": string;
"hanntenn": string;
"hanntennInfo": string;
@ -1767,6 +1769,7 @@ export interface Locale {
};
"_options": {
"gtlAvailable": string;
"emojiPickerProfileLimit": string;
"ltlAvailable": string;
"canPublicNote": string;
"canEditNote": string;

View File

@ -11,6 +11,8 @@ password: "パスワード"
forgotPassword: "パスワードを忘れた"
fetchingAsApObject: "連合に照会中"
ok: "OK"
setDefaultProfileConfirm: "このプロファイルをデフォルトにしますか?"
emojiPickerProfile: "絵文字ピッカーのプロファイル"
notificationIndicator: "通知のインジケーターの数字を表示する"
hanntenn: "アイコンとバナーを反転させる"
hanntennInfo: "ダークだったらライトのアイコンに、ライトだったらダークのアイコンに。"
@ -1673,6 +1675,7 @@ _role:
high: "高"
_options:
gtlAvailable: "グローバルタイムラインの閲覧"
emojiPickerProfileLimit: "絵文字ピッカーのプロファイルの上限数(最大5)"
ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可"
canEditNote: "ノートの編集"

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2023.12.2-PrisMisskey.4",
"version": "2023.12.2-PrisMisskey.5",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -59,6 +59,7 @@ export type RolePolicies = {
userEachUserListsLimit: number;
rateLimitFactor: number;
avatarDecorationLimit: number;
emojiPickerProfileLimit: number;
};
export const DEFAULT_POLICIES: RolePolicies = {
@ -74,7 +75,6 @@ export const DEFAULT_POLICIES: RolePolicies = {
canManageCustomEmojis: false,
canRequestCustomEmojis: false,
canManageAvatarDecorations: false,
canRequestCustomEmojis: false,
canSearchNotes: false,
canUseTranslator: true,
canHideAds: false,
@ -90,6 +90,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
userEachUserListsLimit: 50,
rateLimitFactor: 1,
avatarDecorationLimit: 1,
emojiPickerProfileLimit: 2,
};
@Injectable()
@ -355,6 +356,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)),
rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
emojiPickerProfileLimit: calc('emojiPickerProfileLimit', vs => Math.max(...vs)),
};
}

View File

@ -4,98 +4,103 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" autocapitalize="off" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter">
<!-- FirefoxのTabフォーカスが想定外の挙動となるためtabindex="-1"を追加 https://github.com/misskey-dev/misskey/issues/10744 -->
<div ref="emojisEl" class="emojis" tabindex="-1">
<section class="result">
<div v-if="searchResultCustom.length > 0" class="body">
<button
v-for="emoji in searchResultCustom"
:key="emoji.name"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkCustomEmoji class="emoji" :name="emoji.name"/>
</button>
</div>
<div v-if="searchResultUnicode.length > 0" class="body">
<button
v-for="emoji in searchResultUnicode"
:key="emoji.name"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkEmoji class="emoji" :emoji="emoji.char"/>
</button>
</div>
</section>
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" autocapitalize="off" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter">
<!-- FirefoxのTabフォーカスが想定外の挙動となるためtabindex="-1"を追加 https://github.com/misskey-dev/misskey/issues/10744 -->
<div ref="emojisEl" class="emojis" tabindex="-1">
<section class="result">
<div v-if="searchResultCustom.length > 0" class="body">
<button
v-for="emoji in searchResultCustom"
:key="emoji.name"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkCustomEmoji class="emoji" :name="emoji.name"/>
</button>
</div>
<div v-if="searchResultUnicode.length > 0" class="body">
<button
v-for="emoji in searchResultUnicode"
:key="emoji.name"
class="_button item"
:title="emoji.name"
tabindex="0"
@click="chosen(emoji, $event)"
>
<MkEmoji class="emoji" :emoji="emoji.char"/>
</button>
</div>
</section>
<div v-if="tab === 'index'" class="group index">
<section v-if="showPinned && pinned.length > 0">
<div class="body">
<button
v-for="emoji in pinned"
:key="emoji"
:data-emoji="emoji"
class="_button item"
tabindex="0"
@pointerenter="computeButtonTitle"
@click="chosen(emoji, $event)"
>
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
</button>
</div>
</section>
<div v-if="tab === 'index'" class="group index">
<section v-if="showPinned">
<div style="display: flex; ">
<div v-for="a in profileMax" :key="a" :title="defaultStore.state[`pickerProfileName${a > 1 ? a - 1 : ''}`]" class="sllfktkhgl" :class="{ active: activeIndex === a || isDefaultProfile === a }" @click="pinnedProfileSelect(a)">
{{ defaultStore.state[`pickerProfileName${a > 1 ? a - 1 : ''}`] }}
</div>
</div>
<div class="body">
<button
v-for="emoji in pinnedEmojis"
:key="emoji"
:data-emoji="emoji"
class="_button item"
tabindex="0"
@pointerenter="computeButtonTitle"
@click="chosen(emoji, $event)"
>
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
</button>
</div>
</section>
<section>
<header class="_acrylic"><i class="ti ti-clock ti-fw"></i> {{ i18n.ts.recentUsed }}</header>
<div class="body">
<button
v-for="emoji in recentlyUsedEmojis"
:key="emoji"
class="_button item"
:data-emoji="emoji"
@pointerenter="computeButtonTitle"
@click="chosen(emoji, $event)"
>
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
</button>
</div>
</section>
</div>
<div v-once class="group">
<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
<XSection
v-for="child in customEmojiFolderRoot.children"
:key="`custom:${child.value}`"
:initialShown="false"
:emojis="computed(() => customEmojis.filter(e => child.value === '' ? (e.category === 'null' || !e.category) : e.category === child.value && !customEmojis.some(emoji => emoji.category !== null && emoji.category.includes(e.category+'/')) || e.category === child.category+'/'+child.category && !e.category).filter(filterAvailable).map(e => `:${e.name}:`))"
:hasChildSection="child.children.length !== 0"
:customEmojiTree="child.children"
@chosen="chosen"
>
{{ child.value || i18n.ts.other }}
</XSection>
</div>
<div v-once class="group">
<header class="_acrylic">{{ i18n.ts.emoji }}</header>
<XSection v-for="category in categories" :key="category" :emojis="emojiCharByCategory.get(category) ?? []" :hasChildSection="false" @chosen="chosen">{{ category }}</XSection>
</div>
</div>
<div class="tabs">
<button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="ti ti-asterisk ti-fw"></i></button>
<button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="ti ti-mood-happy ti-fw"></i></button>
<button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="ti ti-leaf ti-fw"></i></button>
<button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="ti ti-hash ti-fw"></i></button>
</div>
</div>
<section>
<header class="_acrylic"><i class="ti ti-clock ti-fw"></i> {{ i18n.ts.recentUsed }}</header>
<div class="body">
<button
v-for="emoji in recentlyUsedEmojis"
:key="emoji"
class="_button item"
:data-emoji="emoji"
@pointerenter="computeButtonTitle"
@click="chosen(emoji, $event)"
>
<MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
<MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
</button>
</div>
</section>
</div>
<div v-once class="group">
<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
<XSection
v-for="child in customEmojiFolderRoot.children"
:key="`custom:${child.value}`"
:initialShown="false"
:emojis="computed(() => customEmojis.filter(e => child.value === '' ? (e.category === 'null' || !e.category) : e.category === child.value && !customEmojis.some(emoji => emoji.category !== null && emoji.category.includes(e.category+'/')) || e.category === child.category+'/'+child.category && !e.category).filter(filterAvailable).map(e => `:${e.name}:`))"
:hasChildSection="child.children.length !== 0"
:customEmojiTree="child.children"
@chosen="chosen"
>
{{ child.value || i18n.ts.other }}
</XSection>
</div>
<div v-once class="group">
<header class="_acrylic">{{ i18n.ts.emoji }}</header>
<XSection v-for="category in categories" :key="category" :emojis="emojiCharByCategory.get(category) ?? []" :hasChildSection="false" @chosen="chosen">{{ category }}</XSection>
</div>
</div>
<div class="tabs">
<button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="ti ti-asterisk ti-fw"></i></button>
<button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="ti ti-mood-happy ti-fw"></i></button>
<button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="ti ti-leaf ti-fw"></i></button>
<button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="ti ti-hash ti-fw"></i></button>
</div>
</div>
</template>
<script lang="ts" setup>
@ -103,12 +108,12 @@ import { ref, shallowRef, computed, watch, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import XSection from '@/components/MkEmojiPicker.section.vue';
import {
emojilist,
emojiCharByCategory,
UnicodeEmojiDef,
unicodeEmojiCategories as categories,
getEmojiName,
CustomEmojiFolderTree,
emojilist,
emojiCharByCategory,
UnicodeEmojiDef,
unicodeEmojiCategories as categories,
getEmojiName,
CustomEmojiFolderTree,
} from '@/scripts/emojilist.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import * as os from '@/os.js';
@ -117,8 +122,10 @@ import { deviceKind } from '@/scripts/device-kind.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
import { $i } from '@/account.js';
import { signinRequired } from '@/account.js';
import MkButton from '@/components/MkButton.vue';
import { deepClone } from '@/scripts/clone.js';
const $i = signinRequired();
const props = withDefaults(defineProps<{
showPinned?: boolean;
pinnedEmojis?: string[];
@ -127,21 +134,21 @@ const props = withDefaults(defineProps<{
asWindow?: boolean;
asReactionPicker?: boolean; // 使使
}>(), {
showPinned: true,
showPinned: true,
});
const emit = defineEmits<{
(ev: 'chosen', v: string): void;
}>();
const profileMax = $i.policies.emojiPickerProfileLimit;
const searchEl = shallowRef<HTMLInputElement>();
const emojisEl = shallowRef<HTMLDivElement>();
const {
emojiPickerScale,
emojiPickerWidth,
emojiPickerHeight,
recentlyUsedEmojis,
emojiPickerScale,
emojiPickerWidth,
emojiPickerHeight,
recentlyUsedEmojis,
} = defaultStore.reactiveState;
const pinned = computed(() => props.pinnedEmojis);
@ -152,81 +159,80 @@ const q = ref<string>('');
const searchResultCustom = ref<Misskey.entities.EmojiSimple[]>([]);
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
const pinnedEmojis = ref(pinned.value);
const customEmojiFolderRoot: CustomEmojiFolderTree = { value: '', category: '', children: [] };
function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree {
const parts = input.split('/').map(p => p.trim()); //
let currentNode: CustomEmojiFolderTree = root; // currentNode root
let includesPart = customEmojis.value.some(emoji => emoji.category !== null && emoji.category.includes(parts[0]+'/')) ;
console.log(includesPart)
if (parts.length === 1 && parts[0] !== '' && includesPart) { // parts 1
parts.push(parts[0]) // parts parts[0] (test category test/test category )
}
const parts = input.split('/').map(p => p.trim()); //
let currentNode: CustomEmojiFolderTree = root; // currentNode root
let includesPart = customEmojis.value.some(emoji => emoji.category !== null && emoji.category.includes(parts[0] + '/'));
if (parts.length === 1 && parts[0] !== '' && includesPart) { // parts 1
parts.push(parts[0]); // parts parts[0] (test category test/test category )
}
for (const part of parts) { // parts
let existingNode = currentNode.children.find((node) => node.value === part); // currentNode children part value node
for (const part of parts) { // parts
let existingNode = currentNode.children.find((node) => node.value === part); // currentNode children part value node
if (!existingNode) { //
const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] }; // node
if (!existingNode) { //
const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] }; // node
currentNode.children.push(newNode); // currentNode children newNode
existingNode = newNode; // existingNode newNode
}
currentNode.children.push(newNode); // currentNode children newNode
existingNode = newNode; // existingNode newNode
}
currentNode = existingNode; // currentNode existingNode
}
return currentNode;
currentNode = existingNode; // currentNode existingNode
}
return currentNode;
}
customEmojiCategories.value.forEach(ec => {
if (ec !== null) {
parseAndMergeCategories(ec, customEmojiFolderRoot);
}
if (ec !== null) {
parseAndMergeCategories(ec, customEmojiFolderRoot);
}
});
parseAndMergeCategories('', customEmojiFolderRoot);
watch(q, () => {
if (emojisEl.value) emojisEl.value.scrollTop = 0;
if (emojisEl.value) emojisEl.value.scrollTop = 0;
if (q.value === '') {
searchResultCustom.value = [];
searchResultUnicode.value = [];
return;
}
if (q.value === '') {
searchResultCustom.value = [];
searchResultUnicode.value = [];
return;
}
const newQ = q.value.replace(/:/g, '').toLowerCase();
const newQ = q.value.replace(/:/g, '').toLowerCase();
const searchCustom = () => {
const max = 100;
const emojis = customEmojis.value;
const matches = new Set<Misskey.entities.EmojiSimple>();
const searchCustom = () => {
const max = 100;
const emojis = customEmojis.value;
const matches = new Set<Misskey.entities.EmojiSimple>();
const exactMatch = emojis.find(emoji => emoji.name === newQ);
if (exactMatch) matches.add(exactMatch);
const exactMatch = emojis.find(emoji => emoji.name === newQ);
if (exactMatch) matches.add(exactMatch);
if (newQ.includes(' ')) { // AND
const keywords = newQ.split(' ');
if (newQ.includes(' ')) { // AND
const keywords = newQ.split(' ');
//
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
//
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
//
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
} else {
if (customEmojisMap.has(newQ)) {
//
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
} else {
if (customEmojisMap.has(newQ)) {
matches.add(customEmojisMap.get(newQ)!);
}
if (matches.size >= max) return matches;
@ -237,211 +243,219 @@ watch(q, () => {
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;for (const emoji of emojis) {
if (emoji.name.startsWith(newQ)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
if (matches.size >= max) return matches; for (const emoji of emojis) {
if (emoji.name.startsWith(newQ)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.aliases.some(alias => alias.startsWith(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.aliases.some(alias => alias.startsWith(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.name.includes(newQ)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.name.includes(newQ)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.aliases.some(alias => alias.includes(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
for (const emoji of emojis) {
if (emoji.aliases.some(alias => alias.includes(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
return matches;
};
return matches;
};
const searchUnicode = () => {
const max = 100;
const emojis = emojilist;
const matches = new Set<UnicodeEmojiDef>();
const searchUnicode = () => {
const max = 100;
const emojis = emojilist;
const matches = new Set<UnicodeEmojiDef>();
const exactMatch = emojis.find(emoji => emoji.name === newQ);
if (exactMatch) matches.add(exactMatch);
const exactMatch = emojis.find(emoji => emoji.name === newQ);
if (exactMatch) matches.add(exactMatch);
if (newQ.includes(' ')) { // AND
const keywords = newQ.split(' ');
if (newQ.includes(' ')) { // AND
const keywords = newQ.split(' ');
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (keywords.every(keyword => emoji.name.includes(keyword))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) {
if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
} else {
for (const emoji of emojis) {
if (emoji.name.startsWith(newQ)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) {
if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
} else {
for (const emoji of emojis) {
if (emoji.name.startsWith(newQ)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) {
if (index[emoji.char].some(k => k.startsWith(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) {
if (index[emoji.char].some(k => k.startsWith(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
for (const emoji of emojis) {
if (emoji.name.includes(newQ)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const emoji of emojis) {
if (emoji.name.includes(newQ)) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) {
if (index[emoji.char].some(k => k.includes(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
}
for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
for (const emoji of emojis) {
if (index[emoji.char].some(k => k.includes(newQ))) {
matches.add(emoji);
if (matches.size >= max) break;
}
}
}
}
return matches;
};
return matches;
};
searchResultCustom.value = Array.from(searchCustom()).filter(filterAvailable);
searchResultUnicode.value = Array.from(searchUnicode());
searchResultCustom.value = Array.from(searchCustom()).filter(filterAvailable);
searchResultUnicode.value = Array.from(searchUnicode());
});
function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
return (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id)));
return (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id)));
}
function focus() {
if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) {
searchEl.value?.focus({
preventScroll: true,
});
}
if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) {
searchEl.value?.focus({
preventScroll: true,
});
}
}
function reset() {
if (emojisEl.value) emojisEl.value.scrollTop = 0;
q.value = '';
if (emojisEl.value) emojisEl.value.scrollTop = 0;
q.value = '';
}
function getKey(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef): string {
return typeof emoji === 'string' ? emoji : 'char' in emoji ? emoji.char : `:${emoji.name}:`;
return typeof emoji === 'string' ? emoji : 'char' in emoji ? emoji.char : `:${emoji.name}:`;
}
/** @see MkEmojiPicker.section.vue */
function computeButtonTitle(ev: MouseEvent): void {
const elm = ev.target as HTMLElement;
const emoji = elm.dataset.emoji as string;
elm.title = getEmojiName(emoji) ?? emoji;
const elm = ev.target as HTMLElement;
const emoji = elm.dataset.emoji as string;
elm.title = getEmojiName(emoji) ?? emoji;
}
function chosen(emoji: any, ev?: MouseEvent) {
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(MkRippleEffect, { x, y }, {}, 'end');
}
const key = getKey(emoji);
emit('chosen', key);
const key = getKey(emoji);
emit('chosen', key);
// 使
if (!pinned.value?.includes(key)) {
let recents = defaultStore.state.recentlyUsedEmojis;
recents = recents.filter((emoji: any) => emoji !== key);
recents.unshift(key);
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
}
// 使
if (!pinned.value?.includes(key)) {
let recents = defaultStore.state.recentlyUsedEmojis;
recents = recents.filter((emoji: any) => emoji !== key);
recents.unshift(key);
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
}
}
function input(): void {
// Using custom input event instead of v-model to respond immediately on
// Android, where composition happens on all languages
// (v-model does not update during composition)
q.value = searchEl.value?.value.trim() ?? '';
// Using custom input event instead of v-model to respond immediately on
// Android, where composition happens on all languages
// (v-model does not update during composition)
q.value = searchEl.value?.value.trim() ?? '';
}
function paste(event: ClipboardEvent): void {
const pasted = event.clipboardData?.getData('text') ?? '';
if (done(pasted)) {
event.preventDefault();
}
const pasted = event.clipboardData?.getData('text') ?? '';
if (done(pasted)) {
event.preventDefault();
}
}
function onEnter(ev: KeyboardEvent) {
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
done();
if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
done();
}
const activeIndex = ref(defaultStore.state.pickerProfileDefault);
pinnedEmojis.value = props.asReactionPicker ? deepClone(defaultStore.state[`reactions${activeIndex.value > 1 ? activeIndex.value - 1 : ''}`]) : deepClone(defaultStore.state[`pinnedEmojis${activeIndex.value > 1 ? activeIndex.value - 1 : ''}`]);
function pinnedProfileSelect(index:number) {
pinnedEmojis.value = props.asReactionPicker ? deepClone(defaultStore.state[`reactions${index > 1 ? index - 1 : ''}`]) : deepClone(defaultStore.state[`pinnedEmojis${index > 1 ? index - 1 : ''}`]);
activeIndex.value = index;
}
function done(query?: string): boolean | void {
if (query == null) query = q.value;
if (query == null || typeof query !== 'string') return;
if (query == null) query = q.value;
if (query == null || typeof query !== 'string') return;
const q2 = query.replace(/:/g, '');
const exactMatchCustom = customEmojisMap.get(q2);
if (exactMatchCustom) {
chosen(exactMatchCustom);
return true;
}
const exactMatchUnicode = emojilist.find(emoji => emoji.char === q2 || emoji.name === q2);
if (exactMatchUnicode) {
chosen(exactMatchUnicode);
return true;
}
if (searchResultCustom.value.length > 0) {
chosen(searchResultCustom.value[0]);
return true;
}
if (searchResultUnicode.value.length > 0) {
chosen(searchResultUnicode.value[0]);
return true;
}
const q2 = query.replace(/:/g, '');
const exactMatchCustom = customEmojisMap.get(q2);
if (exactMatchCustom) {
chosen(exactMatchCustom);
return true;
}
const exactMatchUnicode = emojilist.find(emoji => emoji.char === q2 || emoji.name === q2);
if (exactMatchUnicode) {
chosen(exactMatchUnicode);
return true;
}
if (searchResultCustom.value.length > 0) {
chosen(searchResultCustom.value[0]);
return true;
}
if (searchResultUnicode.value.length > 0) {
chosen(searchResultUnicode.value[0]);
return true;
}
}
onMounted(() => {
focus();
focus();
});
defineExpose({
focus,
reset,
focus,
reset,
});
</script>
@ -685,4 +699,24 @@ left: 0;*/
}
}
}
.sllfktkhgl{
display: inline-block;
padding: 0 4px;
font-size: 12px;
line-height: 32px;
text-align: center;
color: var(--fg);
cursor: pointer;
width: 100%;
transition: transform 0.3s ease;
box-shadow: 0 1.5px 0 var(--divider);
height: 32px;
overflow: hidden;
&:hover {
transform: translateY(1.5px);
}
&.active {
transform: translateY(5px);
}
}
</style>

View File

@ -68,8 +68,6 @@ import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import { hostname } from '@/config.js';
import { multipleSelectUser } from '@/os.js';
const emit = defineEmits<{
(ev: 'ok', selected: Misskey.entities.UserDetailed): void;
(ev: 'cancel'): void;

View File

@ -99,6 +99,7 @@ export const ROLE_POLICIES = [
'userEachUserListsLimit',
'rateLimitFactor',
'avatarDecorationLimit',
'emojiPickerProfileLimit'
] as const;
// なんか動かない

View File

@ -72,6 +72,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.emojiPickerProfileLimit, 'pickerProfileDefault'])">
<template #label>{{ i18n.ts._role._options.emojiPickerProfileLimit }}</template>
<template #suffix>{{ policies.emojiPickerProfileLimit }}</template>
<MkInput v-model="policies.emojiPickerProfileLimit" type="number">
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])">
<template #label>{{ i18n.ts._role._options.inviteLimit }}</template>
<template #suffix>{{ policies.inviteLimit }}</template>

View File

@ -5,6 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
<MkSelect v-model="nowProfileId">
<template #label>{{ i18n.ts.emojiPickerProfile }}</template>
<option v-for="a in profileMax" :key="a" :value="a">{{ a }}. {{ defaultStore.state[`pickerProfileName${a > 1 ? a - 1 : ''}`] }} </option>
</MkSelect>
<MkInput v-model="profileName">
<template #label>{{ i18n.ts.name }}</template>
</MkInput>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-pin"></i></template>
<template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.reaction }})</template>
@ -84,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</MkFolder>
<MkButton inline primary @click="setDefaultProfile"> {{ i18n.ts.default }}</MkButton>
<FormSection>
<template #label>{{ i18n.ts.emojiPickerDisplay }}</template>
@ -139,6 +146,9 @@ import { emojiPicker } from '@/scripts/emoji-picker.js';
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
import MkEmoji from '@/components/global/MkEmoji.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSelect from '@/components/MkSelect.vue';
import { signinRequired } from '@/account.js';
import MkInput from '@/components/MkInput.vue';
const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(defaultStore.state.reactions));
const pinnedEmojis: Ref<string[]> = ref(deepClone(defaultStore.state.pinnedEmojis));
@ -155,6 +165,20 @@ const setDefaultReaction = () => setDefault(pinnedEmojisForReaction);
const removeEmoji = (reaction: string, ev: MouseEvent) => remove(pinnedEmojis, reaction, ev);
const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev);
const setDefaultEmoji = () => setDefault(pinnedEmojis);
const nowProfileId = ref(defaultStore.state.pickerProfileDefault);
const $i = signinRequired();
const profileMax = $i.policies.emojiPickerProfileLimit;
const profileName = ref(defaultStore.state[`pickerProfileName${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]);
pinnedEmojisForReaction.value = deepClone(defaultStore.state[`reactions${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]);
pinnedEmojis.value = deepClone(defaultStore.state[`pinnedEmojis${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]);
profileName.value = deepClone(defaultStore.state[`pickerProfileName${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]);
watch(nowProfileId, () => {
pinnedEmojisForReaction.value = deepClone(defaultStore.state[`reactions${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]);
pinnedEmojis.value = deepClone(defaultStore.state[`pinnedEmojis${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]);
profileName.value = deepClone(defaultStore.state[`pickerProfileName${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]);
});
function previewReaction(ev: MouseEvent) {
reactionPicker.show(getHTMLElement(ev));
@ -226,17 +250,32 @@ function getHTMLElement(ev: MouseEvent): HTMLElement {
}
watch(pinnedEmojisForReaction, () => {
defaultStore.set('reactions', pinnedEmojisForReaction.value);
defaultStore.set(`reactions${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`, pinnedEmojisForReaction.value);
}, {
deep: true,
});
watch(pinnedEmojis, () => {
defaultStore.set('pinnedEmojis', pinnedEmojis.value);
defaultStore.set( `pinnedEmojis${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`, pinnedEmojis.value);
}, {
deep: true,
});
watch(profileName, () => {
defaultStore.set(`pickerProfileName${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`, profileName.value);
}, {
deep: true,
});
async function setDefaultProfile() {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.setDefaultProfileConfirm,
});
if (canceled) return;
await defaultStore.set('pickerProfileDefault', nowProfileId.value);
}
definePageMetadata({
title: i18n.ts.emojiPicker,
icon: 'ti ti-mood-happy',

View File

@ -58,11 +58,10 @@ export const noteActions: NoteAction[] = [];
export const noteViewInterruptors: NoteViewInterruptor[] = [];
export const notePostInterruptors: NotePostInterruptor[] = [];
export const pageViewInterruptors: PageViewInterruptor[] = [];
export const bannerDark='https://files.prismisskey.space/misskey/e088c6d1-b07f-4312-8d41-fee2f64071e9.png'
export const bannerLight ='https://files.prismisskey.space/misskey/85500d2f-41a9-48ff-a737-65d6fdf74604.png'
export const iconDark='https://files.prismisskey.space/misskey/484efc68-de41-4786-b2b6-e5085c31c2c4.webp'
export const iconLight='https://files.prismisskey.space/misskey/c3d722fe-379f-4c85-9414-90c232d53237.webp'
export const bannerDark = 'https://files.prismisskey.space/misskey/e088c6d1-b07f-4312-8d41-fee2f64071e9.png';
export const bannerLight = 'https://files.prismisskey.space/misskey/85500d2f-41a9-48ff-a737-65d6fdf74604.png';
export const iconDark = 'https://files.prismisskey.space/misskey/484efc68-de41-4786-b2b6-e5085c31c2c4.webp';
export const iconLight = 'https://files.prismisskey.space/misskey/c3d722fe-379f-4c85-9414-90c232d53237.webp';
// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう)
// あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない
@ -129,6 +128,74 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'account',
default: [],
},
reactions1: {
where: 'account',
default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
},
pinnedEmojis1: {
where: 'account',
default: [],
},
reactions2: {
where: 'account',
default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
},
pinnedEmojis2: {
where: 'account',
default: [],
},
reactions3: {
where: 'account',
default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
},
pinnedEmojis3: {
where: 'account',
default: [],
},
reactions4: {
where: 'account',
default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
},
pinnedEmojis4: {
where: 'account',
default: [],
},
reactions5: {
where: 'account',
default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
},
pinnedEmojis5: {
where: 'account',
default: [],
},
pickerProfileName: {
where: 'account',
default: 'default',
},
pickerProfileName1: {
where: 'account',
default: '1',
},
pickerProfileName2: {
where: 'account',
default: '2',
},
pickerProfileName3: {
where: 'account',
default: '3',
},
pickerProfileName4: {
where: 'account',
default: '4',
},
pickerProfileName5: {
where: 'account',
default: '5',
},
pickerProfileDefault: {
where: 'account',
default: 1,
},
reactionAcceptance: {
where: 'account',
default: 'nonSensitiveOnly' as 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null,
@ -296,19 +363,19 @@ export const defaultStore = markRaw(new Storage('base', {
},
gamingType: {
where: 'device',
default: 'dark',
default: 'dark',
},
indicatorCounterToggle: {
where: 'device',
default: 'true',
},
bannerUrl:{
bannerUrl: {
where: 'device',
default: bannerDark
default: bannerDark,
},
iconUrl:{
iconUrl: {
where: 'device',
default: iconDark
default: iconDark,
},
instanceTicker: {
where: 'device',
@ -334,9 +401,9 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: [] as string[],
},
enablehanntenn:{
where:'device',
default: false
enablehanntenn: {
where: 'device',
default: false,
},
recentlyUsedUsers: {
where: 'device',
@ -378,39 +445,39 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: 3,
},
specifiedColor:{
specifiedColor: {
where: 'device',
default: '#FFFF64',
},
followerColor:{
followerColor: {
where: 'device',
default: '#FF00FF',
},
homeColor:{
homeColor: {
where: 'device',
default: '#00FFFF',
},
localOnlyColor:{
where:'device',
default: '#2b2c41'
localOnlyColor: {
where: 'device',
default: '#2b2c41',
},
numberOfGamingSpeed: {
where: 'device',
default: 44,
},
onlyAndWithSave:{
onlyAndWithSave: {
where: 'device',
default: false,
},
onlyFiles:{
onlyFiles: {
where: 'device',
default: false,
},
withReplies:{
withReplies: {
where: 'device',
default: true,
},
withRenotes:{
withRenotes: {
where: 'device',
default: true,
},
@ -422,15 +489,15 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
showMediaTimeline:{
showMediaTimeline: {
where: 'device',
default: true,
},
showGlobalTimeline:{
showGlobalTimeline: {
where: 'device',
default: true,
},
showVisibilityColor:{
showVisibilityColor: {
where: 'device',
default: false,
},
@ -557,7 +624,6 @@ export const defaultStore = markRaw(new Storage('base', {
},
}));
// TODO: 他のタブと永続化されたstateを同期
const PREFIX = 'miux:' as const;