Merge branch 'beta'
This commit is contained in:
commit
33e7bb2fc5
|
@ -11,5 +11,4 @@ pipeline:
|
||||||
password:
|
password:
|
||||||
# Secret 'docker_password' needs to be set in the CI settings
|
# Secret 'docker_password' needs to be set in the CI settings
|
||||||
from_secret: docker_password
|
from_secret: docker_password
|
||||||
|
|
||||||
branches: beta
|
branches: beta
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "calckey",
|
"name": "calckey",
|
||||||
"version": "13.1.3-rc",
|
"version": "13.1.3-rc2",
|
||||||
"codename": "aqua",
|
"codename": "aqua",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
|
@ -4,73 +4,69 @@ import { Emojis } from "@/models/index.js";
|
||||||
import { toPunyNullable } from "./convert-host.js";
|
import { toPunyNullable } from "./convert-host.js";
|
||||||
import { IsNull } from "typeorm";
|
import { IsNull } from "typeorm";
|
||||||
|
|
||||||
const legacies: Record<string, string> = {
|
const legacies = new Map([
|
||||||
like: "👍",
|
['like', '👍'],
|
||||||
love: "❤️", // ここに記述する場合は異体字セレクタを入れない <- not that good because modern browsers just display it as the red heart so just convert it to it to not end up with two seperate reactions of "the same emoji" for the user
|
['love', '❤️'],
|
||||||
laugh: "😆",
|
['laugh', '😆'],
|
||||||
hmm: "🤔",
|
['hmm', '🤔'],
|
||||||
surprise: "😮",
|
['surprise', '😮'],
|
||||||
congrats: "🎉",
|
['congrats', '🎉'],
|
||||||
angry: "💢",
|
['angry', '💢'],
|
||||||
confused: "😥",
|
['confused', '😥'],
|
||||||
rip: "😇",
|
['rip', '😇'],
|
||||||
pudding: "🍮",
|
['pudding', '🍮'],
|
||||||
star: "⭐",
|
['star', '⭐'],
|
||||||
};
|
]);
|
||||||
|
|
||||||
export async function getFallbackReaction(): Promise<string> {
|
export async function getFallbackReaction() {
|
||||||
const meta = await fetchMeta();
|
const meta = await fetchMeta();
|
||||||
return meta.defaultReaction;
|
return meta.defaultReaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertLegacyReactions(reactions: Record<string, number>) {
|
export function convertLegacyReactions(reactions: Record<string, number>) {
|
||||||
const _reactions = {} as Record<string, number>;
|
const _reactions = new Map();
|
||||||
|
const decodedReactions = new Map();
|
||||||
|
|
||||||
for (const reaction of Object.keys(reactions)) {
|
for (const reaction in reactions) {
|
||||||
if (reactions[reaction] <= 0) continue;
|
if (reactions[reaction] <= 0) continue;
|
||||||
|
|
||||||
if (Object.keys(legacies).includes(reaction)) {
|
let decodedReaction;
|
||||||
if (_reactions[legacies[reaction]]) {
|
if (decodedReactions.has(reaction)) {
|
||||||
_reactions[legacies[reaction]] += reactions[reaction];
|
decodedReaction = decodedReactions.get(reaction);
|
||||||
} else {
|
|
||||||
_reactions[legacies[reaction]] = reactions[reaction];
|
|
||||||
}
|
|
||||||
} else if (reaction === "♥️") {
|
|
||||||
if (_reactions["❤️"]) {
|
|
||||||
_reactions["❤️"] += reactions[reaction];
|
|
||||||
} else {
|
|
||||||
_reactions["❤️"] = reactions[reaction];
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (_reactions[reaction]) {
|
decodedReaction = decodeReaction(reaction);
|
||||||
_reactions[reaction] += reactions[reaction];
|
decodedReactions.set(reaction, decodedReaction);
|
||||||
} else {
|
}
|
||||||
_reactions[reaction] = reactions[reaction];
|
|
||||||
}
|
let emoji = legacies.get(decodedReaction.reaction);
|
||||||
|
if (emoji) {
|
||||||
|
_reactions.set(emoji, (_reactions.get(emoji) || 0) + reactions[reaction]);
|
||||||
|
} else {
|
||||||
|
_reactions.set(reaction, (_reactions.get(reaction) || 0) + reactions[reaction]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const _reactions2 = {} as Record<string, number>;
|
const _reactions2 = new Map();
|
||||||
|
for (const [reaction, count] of _reactions) {
|
||||||
for (const reaction of Object.keys(_reactions)) {
|
const decodedReaction = decodedReactions.get(reaction);
|
||||||
_reactions2[decodeReaction(reaction).reaction] = _reactions[reaction];
|
_reactions2.set(decodedReaction.reaction, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _reactions2;
|
return Object.fromEntries(_reactions2);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function toDbReaction(
|
export async function toDbReaction(
|
||||||
reaction?: string | null,
|
reaction?: string | null,
|
||||||
reacterHost?: string | null,
|
reacterHost?: string | null,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (reaction == null) return await getFallbackReaction();
|
if (!reaction) return await getFallbackReaction();
|
||||||
|
|
||||||
reacterHost = toPunyNullable(reacterHost);
|
reacterHost = toPunyNullable(reacterHost);
|
||||||
|
|
||||||
// Convert string-type reactions to unicode
|
// Convert string-type reactions to unicode
|
||||||
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
const emoji = legacies.get(reaction) || (reaction === "♥️" ? "❤️" : null);
|
||||||
// Convert old heart to new
|
if (emoji) return emoji;
|
||||||
if (reaction === "♥️") return "❤️";
|
|
||||||
// Allow unicode reactions
|
// Allow unicode reactions
|
||||||
const match = emojiRegex.exec(reaction);
|
const match = emojiRegex.exec(reaction);
|
||||||
if (match) {
|
if (match) {
|
||||||
|
@ -82,7 +78,7 @@ export async function toDbReaction(
|
||||||
if (custom) {
|
if (custom) {
|
||||||
const name = custom[1];
|
const name = custom[1];
|
||||||
const emoji = await Emojis.findOneBy({
|
const emoji = await Emojis.findOneBy({
|
||||||
host: reacterHost ?? IsNull(),
|
host: reacterHost || IsNull(),
|
||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -131,7 +127,7 @@ export function decodeReaction(str: string): DecodedReaction {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function convertLegacyReaction(reaction: string): string {
|
export function convertLegacyReaction(reaction: string): string {
|
||||||
reaction = decodeReaction(reaction).reaction;
|
const decoded = decodeReaction(reaction).reaction;
|
||||||
if (Object.keys(legacies).includes(reaction)) return legacies[reaction];
|
if (legacies.has(decoded)) return legacies.get(decoded)!;
|
||||||
return reaction;
|
return decoded;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,35 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="show" ref="el" class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick">
|
<div v-if="show" ref="el" class="fdidabkb" :class="{ slim: narrow, thin: thin_ }" :style="{ background: bg }" @click="onClick">
|
||||||
<div v-if="narrow" class="buttons left" @click="openAccountMenu">
|
<div v-if="narrow" class="buttons left" @click="openAccountMenu">
|
||||||
<MkAvatar v-if="props.displayMyAvatar && $i" class="avatar" :user="$i" :disable-preview="true"/>
|
<MkAvatar v-if="props.displayMyAvatar && $i" class="avatar" :user="$i" :disable-preview="true"/>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="metadata">
|
<template v-if="metadata">
|
||||||
<div v-if="!hideTitle" class="titleContainer" @click="showTabsPopup">
|
<div v-if="!hideTitle" class="titleContainer" @click="showTabsPopup">
|
||||||
<MkAvatar v-if="metadata.avatar" class="avatar" :user="metadata.avatar" :disable-preview="true" :show-indicator="true"/>
|
<MkAvatar v-if="metadata.avatar" class="avatar" :user="metadata.avatar" :disable-preview="true" :show-indicator="true"/>
|
||||||
<i v-else-if="metadata.icon && !narrow" class="icon" :class="metadata.icon"></i>
|
<i v-else-if="metadata.icon && !narrow" class="icon" :class="metadata.icon"></i>
|
||||||
|
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true" class="title"/>
|
<MkUserName v-if="metadata.userName" :user="metadata.userName" :nowrap="true" class="title"/>
|
||||||
<div v-else-if="metadata.title && !(tabs != null && tabs.length > 0 && narrow)" class="title">{{ metadata.title }}</div>
|
<div v-else-if="metadata.title && !(tabs != null && tabs.length > 0 && narrow)" class="title">{{ metadata.title }}</div>
|
||||||
<div v-if="!narrow && metadata.subtitle" class="subtitle">
|
<div v-if="!narrow && metadata.subtitle" class="subtitle">
|
||||||
{{ metadata.subtitle }}
|
{{ metadata.subtitle }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div ref="tabsEl" v-if="hasTabs" class="tabs">
|
||||||
<div ref="tabsEl" v-if="hasTabs" class="tabs">
|
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip.noDelay="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
|
||||||
<button v-for="tab in tabs" :ref="(el) => tabRefs[tab.key] = el" v-tooltip.noDelay="tab.title" class="tab _button" :class="{ active: tab.key != null && tab.key === props.tab }" @mousedown="(ev) => onTabMousedown(tab, ev)" @click="(ev) => onTabClick(tab, ev)">
|
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
|
||||||
<i v-if="tab.icon" class="icon" :class="tab.icon"></i>
|
<span class="title">{{ tab.title }}</span>
|
||||||
<span v-if="deviceKind !== 'desktop' || isTouchUsing || (!tab.iconOnly && !narrow)" class="title">{{ tab.title }}</span>
|
</button>
|
||||||
</button>
|
<div ref="tabHighlightEl" class="highlight"></div>
|
||||||
<div ref="tabHighlightEl" class="highlight"></div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="buttons right">
|
|
||||||
<template v-for="action in actions">
|
|
||||||
<button v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
|
|
||||||
</template>
|
</template>
|
||||||
|
<div class="buttons right">
|
||||||
|
<template v-for="action in actions">
|
||||||
|
<button v-tooltip.noDelay="action.text" class="_button button" :class="{ highlighted: action.highlighted }" @click.stop="action.handler" @touchstart="preventDrag"><i :class="action.icon"></i></button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
@ -37,10 +37,7 @@ import { computed, onMounted, onUnmounted, ref, inject, watch, shallowReactive,
|
||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { popupMenu } from '@/os';
|
import { popupMenu } from '@/os';
|
||||||
import { scrollToTop } from '@/scripts/scroll';
|
import { scrollToTop } from '@/scripts/scroll';
|
||||||
import { i18n } from '@/i18n';
|
|
||||||
import { globalEvents } from '@/events';
|
import { globalEvents } from '@/events';
|
||||||
import { deviceKind } from '@/scripts/device-kind';
|
|
||||||
import { isTouchUsing } from '@/scripts/touch';
|
|
||||||
import { injectPageMetadata } from '@/scripts/page-metadata';
|
import { injectPageMetadata } from '@/scripts/page-metadata';
|
||||||
import { $i, openAccountMenu as openAccountMenu_ } from '@/account';
|
import { $i, openAccountMenu as openAccountMenu_ } from '@/account';
|
||||||
|
|
||||||
|
@ -153,12 +150,18 @@ onMounted(() => {
|
||||||
if (tabEl && tabHighlightEl) {
|
if (tabEl && tabHighlightEl) {
|
||||||
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
// offsetWidth や offsetLeft は少数を丸めてしまうため getBoundingClientRect を使う必要がある
|
||||||
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
|
// https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/offsetWidth#%E5%80%A4
|
||||||
const parentRect = tabsEl.getBoundingClientRect();
|
const tabSizeX = tabEl.scrollWidth + 20; // + the tab's padding
|
||||||
const rect = tabEl.getBoundingClientRect();
|
tabEl.style = `--width: ${tabSizeX}px`;
|
||||||
const left = (rect.left - parentRect.left + tabsEl?.scrollLeft);
|
setTimeout(() => {
|
||||||
tabHighlightEl.style.width = rect.width + 'px';
|
const parentRect = tabsEl.getBoundingClientRect();
|
||||||
tabHighlightEl.style.left = left + 'px';
|
const rect = tabEl.getBoundingClientRect();
|
||||||
tabsEl.scrollTo({left: left - 80, behavior: "smooth"});
|
const left = (rect.left - parentRect.left + tabsEl?.scrollLeft);
|
||||||
|
tabHighlightEl.style.width = tabSizeX + 'px';
|
||||||
|
tabHighlightEl.style.transform = `translateX(${left}px)`;
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
tabsEl?.scrollTo({left: left - 60, behavior: "smooth"});
|
||||||
|
})
|
||||||
|
}, 200);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, {
|
}, {
|
||||||
|
@ -190,7 +193,6 @@ onUnmounted(() => {
|
||||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||||
backdrop-filter: var(--blur, blur(15px));
|
backdrop-filter: var(--blur, blur(15px));
|
||||||
border-bottom: solid 0.5px var(--divider);
|
border-bottom: solid 0.5px var(--divider);
|
||||||
contain: strict;
|
|
||||||
height: var(--height);
|
height: var(--height);
|
||||||
|
|
||||||
&.thin {
|
&.thin {
|
||||||
|
@ -235,7 +237,7 @@ onUnmounted(() => {
|
||||||
> .buttons {
|
> .buttons {
|
||||||
--margin: 8px;
|
--margin: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: var(--height);
|
height: var(--height);
|
||||||
margin: 0 var(--margin);
|
margin: 0 var(--margin);
|
||||||
|
|
||||||
|
@ -349,16 +351,20 @@ onUnmounted(() => {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
contain: strict;
|
||||||
|
|
||||||
> .tab {
|
> .tab {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 10px;
|
border-inline: 10px solid transparent;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
transition: color .2s, opacity .2s;
|
width: 38px;
|
||||||
|
--width: 38px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: color .2s, opacity .2s, width .2s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -368,20 +374,29 @@ onUnmounted(() => {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
width: var(--width);
|
||||||
|
}
|
||||||
|
&:not(.active) > .title {
|
||||||
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .icon + .title {
|
> .icon + .title {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
> .title {
|
||||||
|
transition: opacity .2s;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .highlight {
|
> .highlight {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
transition: all 0.2s ease;
|
transition: width .2s, transform .2s;
|
||||||
|
transition-timing-function: cubic-bezier(0,0,0,1.2);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -315,12 +315,14 @@ onMounted(() => {
|
||||||
top: calc(var(--stickyTop, 0px) + 16px);
|
top: calc(var(--stickyTop, 0px) + 16px);
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
> button {
|
> button {
|
||||||
display: block;
|
display: block;
|
||||||
margin: var(--margin) auto 0 auto;
|
margin: var(--margin) auto 0 auto;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue