Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/tuikit-atomicx-vue3/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tuikit-atomicx-vue3",
"version": "6.2.2",
"version": "6.2.6",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
Expand Down Expand Up @@ -40,7 +40,7 @@
"@tencentcloud/lite-chat": "^1.6.15",
"@tencentcloud/chat-uikit-engine-lite": "~1.0.7",
"@tencentcloud/tui-core-lite": "~1.0.1",
"@tencentcloud/tuiroom-engine-js": "~4.1.2-beta.2",
"@tencentcloud/tuiroom-engine-js": "~4.1.2-beta.4",
"@tencentcloud/uikit-base-component-vue3": "~1.4.4",
"vue": "^3.4.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
export const ERROR_MESSAGE = {
10017: 'BarrageInput.youHaveBeenMuted',
// Server-side moderation rejected the message because the text contains
// sensitive content (e.g. forbidden keywords). Surfaced as a TUIToast
// error in BarrageInput / TextEditor so the user knows the message was
// not delivered to the room.
80001: 'BarrageInput.sensitiveContent',
// Server-side moderation rejected the message because attached media
// (image / audio / video) contains sensitive content. Distinct from
// 80001 (text) so the toast can tell the user which part was rejected.
80004: 'BarrageInput.sensitiveMediaContent',
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export const resource = {
'BarrageInput.youHaveBeenMuted': 'You have been muted',
'BarrageInput.Send': 'Send',
'BarrageInput.sendFailed': 'Send failed',
'BarrageInput.sensitiveContent': 'The message contains sensitive content and cannot be sent.',
'BarrageInput.sensitiveMediaContent': 'Images, audio, video or other media in the message contain sensitive content and cannot be sent.',
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export const resource = {
'BarrageInput.youHaveBeenMuted': '您已被禁言',
'BarrageInput.Send': '发送',
'BarrageInput.sendFailed': '发送失败',
'BarrageInput.sensitiveContent': '消息或者资料中文本存在敏感内容,禁止下发。',
'BarrageInput.sensitiveMediaContent': '消息中图片、音频、视频等文件存在敏感内容,禁止下发。',
};
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,11 @@ onUnmounted(() => {
border-radius: 50%;
background: var(--gift-more-button-color);
cursor: pointer;
// The "More gifts" round button always uses a colored fill, so its inner
// icon (IconGift) must stay white regardless of light/dark theme to keep
// sufficient contrast. The icon renders with `fill: currentColor`, so we
// pin `color` on the wrapper instead of touching the icon component.
color: #fff;
}

span {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
:key="`seat-${index}`"
:style="item.region"
>
<div class="battle-decorate" v-if="getBattleLevel(item.userInfo.userId) > 0">
<div
class="battle-decorate"
v-if="getBattleLevel(item.userInfo.userId) > 0"
:style="{ '--widget-scale': getSeatScale(item.region) }"
>
<span class="battle-score-value" v-if="!battleScore?.has(item.userInfo.userId)">{{ t('LiveView.Connecting') }}</span>
<template v-else>
<div class="battle-badge-container" :class="getBattleLevel(item.userInfo.userId) === 1 ? 'top-badge' : 'ordinary-badge'">
Expand All @@ -31,6 +35,7 @@ import { ref, computed, watch } from 'vue';
import BattleTopBadge from '../assets/svg/BattleTopBadge.svg';
import BattleOrdinaryBadge from '../assets/svg/BattleOrdinaryBadge.svg';
import { useUIKit } from '@tencentcloud/uikit-base-component-vue3';
import { getWidgetScale } from '../useWidgetScale';

const { t } = useUIKit();

Expand Down Expand Up @@ -65,17 +70,18 @@ function getBattleLevel(userId: string) {
return currentBattleScoreList.value.indexOf(battleScore.value.get(userId) || 0) + 1;
};

let battleTimer: NodeJS.Timeout | null = null;
watch(() => currentBattleInfo.value?.battleId, (newVal) => {
if(newVal !== null && newVal !== undefined) {
isInBattle.value = true;
} else {
if(battleTimer) return;
battleTimer = setTimeout(() => {
isInBattle.value = false;
}, 5000);
}
}, { immediate: true });
// Lower bound for the PK badge scale. Higher than the text floor (0.5) so the
// badge graphics / score stay clear instead of collapsing on tiny seats.
const BADGE_WIDGET_MIN_SCALE = 0.6;

// Per-seat scale derived from the region size provided by the parent layout,
// so the PK badge shrinks proportionally on small seats.
function getSeatScale(region: { width: string; height: string }) {
return getWidgetScale(
{ width: parseInt(region.width), height: parseInt(region.height) },
{ min: BADGE_WIDGET_MIN_SCALE },
);
}

</script>

Expand Down Expand Up @@ -103,6 +109,8 @@ watch(() => currentBattleInfo.value?.battleId, (newVal) => {
background-color: rgba(15, 16, 20, 0.4);
border-radius: 24px;
color: var(--text-color-primary);
transform: scale(var(--widget-scale, 1));
transform-origin: top left;
.battle-badge-container {
display: flex;
align-items: center;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
:src="userInfo?.avatarUrl"
/>
</div>
<div v-if="seatListWithUser.length > 1" class="user-details">
<div
v-if="seatListWithUser.length > 1"
class="user-details"
:style="{ '--widget-scale': widgetScale }"
>
<AudioIcon
v-if="!isAudioAvailable"
class="audio-icon"
Expand All @@ -31,7 +35,7 @@
class="empty-position"
:class="{ 'clickable': !isAnchor }"
>
<div class="seat-display">
<div class="seat-display" :style="{ '--widget-scale': widgetScale }">
<IconPlus v-if="!isAnchor" />
<span v-else class="seat-index">{{ props.seatIndex }}</span>
<span class="text">{{ isAnchor ? t('LiveView.WaitingForConnection') : t('LiveView.ApplyForConnection') }}</span>
Expand All @@ -49,6 +53,7 @@ import { useLiveSeatState } from '../../states/LiveSeatState';
import { useLoginState } from '../../states/LoginState';
import { DeviceStatus } from '../../types';
import { Avatar } from '../Avatar';
import { useWidgetScale } from './useWidgetScale';
import type { SeatUserInfo } from '../../types';

interface Props {
Expand Down Expand Up @@ -82,7 +87,10 @@ const isAnchor = computed(() => {
const seatListWithUser = computed(() => seatList.value.filter(item => item.userInfo && item.userInfo.userId !== ''));

const currentStreamViewSize = computed(() => {
const currentStreamViewInfo = props.streamViewInfoList.find(item => item.userInfo?.userId === props.userInfo?.userId);
// Match by userId when occupied; fall back to the positional region
// (seatIndex - 1) so empty seats can still derive their size for scaling.
const currentStreamViewInfo = props.streamViewInfoList.find(item => item.userInfo?.userId === props.userInfo?.userId)
?? props.streamViewInfoList[props.seatIndex - 1];
if (!currentStreamViewInfo) {
return { width: 0, height: 0 };
}
Expand All @@ -101,6 +109,15 @@ const avatarSize = computed(() => {
return defaultAvatarSize;
});

// Lower bound for the text-overlay scale. Smaller than the badge floor (0.6)
// because wrapped captions stay readable even when shrunk further.
const TEXT_WIDGET_MIN_SCALE = 0.5;

// Uniform scale for overlay widgets so they shrink proportionally on small
// seats. Text always stays visible (wrapping instead of being clipped),
// honoring the goal of keeping captions fully readable.
const widgetScale = useWidgetScale(currentStreamViewSize, { min: TEXT_WIDGET_MIN_SCALE });

const needCanvasMaskList = computed(() => {
const currentStreamViewInfo = props.streamViewInfoList.find(item => item.userInfo?.userId === props.userInfo?.userId);
if (!currentStreamViewInfo) {
Expand Down Expand Up @@ -232,6 +249,8 @@ const isVideoAvailable = computed(() => props.userInfo?.cameraStatus === DeviceS
border-radius: 100px;
max-width: 80%;
box-sizing: border-box;
transform: scale(var(--widget-scale, 1));
transform-origin: bottom left;

.audio-icon {
zoom: 0.6;
Expand All @@ -241,9 +260,13 @@ const isVideoAvailable = computed(() => props.userInfo?.cameraStatus === DeviceS
font-size: 12px;
font-weight: 500;
margin-left: 2px;
white-space: nowrap;
// Allow up to two lines so longer names stay mostly readable, while
// still capping growth to avoid an ever-taller pill covering the video.
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
}
}

Expand All @@ -270,9 +293,11 @@ const isVideoAvailable = computed(() => props.userInfo?.cameraStatus === DeviceS
.text {
font-size: 14px;
max-width: 80%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
// Wrap freely instead of clipping: the empty seat lays out vertically
// and has spare height, so the full prompt stays readable on small seats.
white-space: normal;
word-break: break-word;
text-align: center;
color: var(--text-color-primary);
font-weight: 400;
}
Expand All @@ -284,6 +309,8 @@ const isVideoAvailable = computed(() => props.userInfo?.cameraStatus === DeviceS
color: var(--text-color-primary);
align-items: center;
gap: 12px;
transform: scale(var(--widget-scale, 1));
transform-origin: center center;
}

.seat-index {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { computed, type Ref } from 'vue';

// Default reference seat short side (px) at which the scale equals 1. Seats
// whose short side is >= this stay 1:1; smaller seats shrink proportionally.
// Tuned to the widgets' actual footprint so reasonably sized seats are not
// scaled down unnecessarily.
const DEFAULT_WIDGET_SCALE_BASE = 120;

export interface WidgetScaleOptions {
// Reference seat short side (px) at which the scale equals 1.
base?: number;
// Lower bound to keep widgets readable on tiny seats.
min?: number;
}

// Derive a uniform scale factor from a seat size so overlay widgets shrink
// proportionally on small seats instead of being clipped by ellipsis or
// overflowing the seat region. Returns 1 when the size is unknown.
export function useWidgetScale(
size: Ref<{ width: number; height: number }>,
options: WidgetScaleOptions = {},
) {
return computed(() => getWidgetScale(size.value, options));
}

// Imperative variant for cases where the size comes from a v-for item rather
// than a reactive ref (e.g. per-seat regions provided by the parent layout).
export function getWidgetScale(
size: { width: number; height: number },
options: WidgetScaleOptions = {},
) {
const { base = DEFAULT_WIDGET_SCALE_BASE, min = 0.6 } = options;
const minSide = Math.min(size.width, size.height);
if (!minSide || Number.isNaN(minSide)) {
return 1;
}
return Math.max(min, Math.min(1, minSide / base));
}
Loading