mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-19 06:43:51 +02:00
fix friends locations
This commit is contained in:
@@ -58,99 +58,45 @@
|
||||
<div v-else class="friend-view__toolbar friend-view__toolbar--loading">
|
||||
<span class="friend-view__loading-text">{{ t('view.friends_locations.loading_more') }}</span>
|
||||
</div>
|
||||
<ScrollArea v-if="settingsReady" ref="scrollbarRef" class="friend-view__scroll" @scroll="handleScroll">
|
||||
<template v-if="isSameInstanceView">
|
||||
<div v-if="visibleSameInstanceGroups.length" class="friend-view__instances">
|
||||
<section
|
||||
v-for="group in visibleSameInstanceGroups"
|
||||
:key="group.instanceId"
|
||||
class="friend-view__instance">
|
||||
<header class="friend-view__instance-header">
|
||||
<Location class="extra" :location="group.instanceId" style="display: inline" />
|
||||
<span class="friend-view__instance-count">{{ group.friends.length }}</span>
|
||||
</header>
|
||||
<div
|
||||
class="friend-view__grid"
|
||||
:style="
|
||||
gridStyle(group.friends.length, {
|
||||
preferredColumns: sameInstanceColumnTarget,
|
||||
disableAutoStretch: true,
|
||||
matchMaxColumnWidth: true
|
||||
})
|
||||
">
|
||||
<FriendLocationCard
|
||||
v-for="friend in group.friends"
|
||||
:key="friend.id ?? friend.userId ?? friend.displayName"
|
||||
:friend="friend"
|
||||
:card-scale="cardScale"
|
||||
:card-spacing="cardSpacing" />
|
||||
</div>
|
||||
</section>
|
||||
<ScrollArea v-if="settingsReady" ref="scrollbarRef" class="friend-view__scroll">
|
||||
<div v-if="virtualRows.length" class="friend-view__virtual" :style="virtualListStyle">
|
||||
<div class="friend-view__virtual-spacer" :style="virtualSpacerStyle">
|
||||
<div
|
||||
v-for="vRow in virtualItems"
|
||||
:key="String(virtualRows[vRow.index]?.key ?? vRow.key)"
|
||||
class="friend-view__virtual-row"
|
||||
:data-index="vRow.index"
|
||||
:ref="(el) => onVirtualRowRef(el)"
|
||||
:style="virtualRowStyle(vRow.start)">
|
||||
<template v-if="virtualRows[vRow.index]?.type === 'header'">
|
||||
<header class="friend-view__instance-header">
|
||||
<Location
|
||||
class="extra"
|
||||
:location="virtualRows[vRow.index].instanceId"
|
||||
style="display: inline" />
|
||||
<span class="friend-view__instance-count">{{ virtualRows[vRow.index].count }}</span>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<template v-else-if="virtualRows[vRow.index]?.type === 'divider'">
|
||||
<div class="friend-view__divider"><span class="friend-view__divider-text"></span></div>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<div class="friend-view__row">
|
||||
<FriendLocationCard
|
||||
v-for="item in virtualRows[vRow.index]?.items ?? []"
|
||||
:key="item.key"
|
||||
:friend="item.friend"
|
||||
:card-scale="cardScale"
|
||||
:card-spacing="cardSpacing"
|
||||
:display-instance-info="item.displayInstanceInfo" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="friend-view__empty">{{ t('view.friends_locations.no_matching_friends') }}</div>
|
||||
</template>
|
||||
<template v-else-if="shouldMergeSameInstance">
|
||||
<div v-if="mergedSameInstanceGroups.length" class="friend-view__instances">
|
||||
<section
|
||||
v-for="group in mergedSameInstanceGroups"
|
||||
:key="group.instanceId"
|
||||
class="friend-view__instance">
|
||||
<header class="friend-view__instance-header">
|
||||
<Location class="extra" :location="group.instanceId" style="display: inline" />
|
||||
<span class="friend-view__instance-count">{{ group.friends.length }}</span>
|
||||
</header>
|
||||
<div
|
||||
class="friend-view__grid"
|
||||
:style="
|
||||
gridStyle(group.friends.length, {
|
||||
preferredColumns: sameInstanceColumnTarget,
|
||||
disableAutoStretch: true,
|
||||
matchMaxColumnWidth: true
|
||||
})
|
||||
">
|
||||
<FriendLocationCard
|
||||
v-for="friend in group.friends"
|
||||
:key="friend.id ?? friend.userId ?? friend.displayName"
|
||||
:friend="friend"
|
||||
:card-scale="cardScale"
|
||||
:card-spacing="cardSpacing"
|
||||
:display-instance-info="false" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div v-if="mergedSameInstanceGroups.length && mergedOnlineEntries.length" class="friend-view__divider">
|
||||
<span class="friend-view__divider-text"></span>
|
||||
</div>
|
||||
<div
|
||||
v-if="mergedOnlineEntries.length"
|
||||
class="friend-view__grid"
|
||||
:style="gridStyle(mergedOnlineEntries.length)">
|
||||
<FriendLocationCard
|
||||
v-for="entry in mergedOnlineEntries"
|
||||
:key="entry.id ?? entry.friend.id ?? entry.friend.displayName"
|
||||
:friend="entry.friend"
|
||||
:card-scale="cardScale"
|
||||
:card-spacing="cardSpacing" />
|
||||
</div>
|
||||
<div v-if="!mergedSameInstanceGroups.length && !mergedOnlineEntries.length" class="friend-view__empty">
|
||||
{{ t('view.friends_locations.no_matching_friends') }}
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="visibleFriends.length" class="friend-view__grid" :style="gridStyle(visibleFriends.length)">
|
||||
<FriendLocationCard
|
||||
v-for="entry in visibleFriends"
|
||||
:key="entry.id ?? entry.friend.id ?? entry.friend.displayName"
|
||||
:friend="entry.friend"
|
||||
:card-scale="cardScale"
|
||||
:card-spacing="cardSpacing" />
|
||||
</div>
|
||||
<div v-else class="friend-view__empty">{{ t('view.friends_locations.no_matching_friends') }}</div>
|
||||
</template>
|
||||
<div v-if="isLoadingMore" class="friend-view__loading">
|
||||
<Loader2 class="friend-view__loading-icon" :size="18" />
|
||||
<span>{{ t('view.friends_locations.loading_more') }}</span>
|
||||
</div>
|
||||
<div v-else class="friend-view__empty">{{ t('view.friends_locations.no_matching_friends') }}</div>
|
||||
</ScrollArea>
|
||||
<div v-else class="friend-view__initial-loading">
|
||||
<Loader2 class="friend-view__loading-icon" :size="22" />
|
||||
@@ -159,7 +105,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onBeforeMount, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Loader2, Settings } from 'lucide-vue-next';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -167,6 +113,7 @@
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useVirtualizer } from '@tanstack/vue-virtual';
|
||||
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '../../components/ui/popover';
|
||||
import { Slider } from '../../components/ui/slider';
|
||||
@@ -249,21 +196,17 @@
|
||||
|
||||
const settingsReady = ref(false);
|
||||
|
||||
const PAGE_SIZE = 18;
|
||||
const VIEWPORT_BUFFER = 32;
|
||||
|
||||
const activeSegment = ref('online');
|
||||
const searchTerm = ref('');
|
||||
|
||||
const itemsToShow = ref(PAGE_SIZE);
|
||||
const isLoadingMore = ref(false);
|
||||
const scrollbarRef = ref();
|
||||
const scrollViewportRef = shallowRef(null);
|
||||
const gridWidth = ref(0);
|
||||
let resizeObserver;
|
||||
let cleanupResize;
|
||||
|
||||
const updateGridWidth = () => {
|
||||
const wrap = scrollbarRef.value?.viewportEl?.value;
|
||||
const wrap = scrollViewportRef.value;
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
@@ -277,7 +220,7 @@
|
||||
cleanupResize = undefined;
|
||||
}
|
||||
|
||||
const wrap = scrollbarRef.value?.viewportEl?.value;
|
||||
const wrap = scrollViewportRef.value;
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
@@ -418,8 +361,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
const visibleFriends = computed(() => filteredFriends.value.slice(0, itemsToShow.value));
|
||||
|
||||
const isSameInstanceView = computed(
|
||||
() => showSameInstance.value && activeSegment.value === 'same-instance' && !normalizedSearchTerm.value
|
||||
);
|
||||
@@ -454,56 +395,34 @@
|
||||
}));
|
||||
};
|
||||
|
||||
const visibleSameInstanceGroups = computed(() => {
|
||||
const sameInstanceGroupsForVirtual = computed(() => {
|
||||
if (!isSameInstanceView.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return buildSameInstanceGroups(visibleFriends.value);
|
||||
return buildSameInstanceGroups(filteredFriends.value);
|
||||
});
|
||||
|
||||
const mergedSameInstanceEntries = computed(() => {
|
||||
if (!shouldMergeSameInstance.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return visibleFriends.value.filter((entry) => entry.section === 'same-instance');
|
||||
return filteredFriends.value.filter((entry) => entry.section === 'same-instance');
|
||||
});
|
||||
|
||||
const mergedOnlineEntries = computed(() => {
|
||||
if (!shouldMergeSameInstance.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return visibleFriends.value.filter((entry) => entry.section !== 'same-instance');
|
||||
return filteredFriends.value.filter((entry) => entry.section !== 'same-instance');
|
||||
});
|
||||
|
||||
const mergedSameInstanceGroups = computed(() => {
|
||||
if (!shouldMergeSameInstance.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return buildSameInstanceGroups(mergedSameInstanceEntries.value);
|
||||
});
|
||||
|
||||
const sameInstanceColumnTarget = computed(() => {
|
||||
const groups = isSameInstanceView.value
|
||||
? visibleSameInstanceGroups.value
|
||||
: shouldMergeSameInstance.value
|
||||
? mergedSameInstanceGroups.value
|
||||
: [];
|
||||
|
||||
let maxCount = 0;
|
||||
for (const group of groups) {
|
||||
const size = Array.isArray(group?.friends) ? group.friends.length : 0;
|
||||
if (size > maxCount) {
|
||||
maxCount = size;
|
||||
}
|
||||
}
|
||||
|
||||
return maxCount > 0 ? maxCount : null;
|
||||
});
|
||||
|
||||
const gridStyle = computed(() => {
|
||||
const baseWidth = 220;
|
||||
const baseGap = 14;
|
||||
@@ -551,92 +470,191 @@
|
||||
};
|
||||
});
|
||||
|
||||
const handleScroll = () => {
|
||||
if (
|
||||
isLoadingMore.value ||
|
||||
filteredFriends.value.length === 0 ||
|
||||
itemsToShow.value >= filteredFriends.value.length
|
||||
) {
|
||||
function resolveScrollViewport() {
|
||||
const rootEl = scrollbarRef.value?.$el ?? null;
|
||||
if (!rootEl) {
|
||||
scrollViewportRef.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const wrap = scrollbarRef.value?.viewportEl?.value;
|
||||
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scrollHeight, scrollTop, clientHeight } = wrap;
|
||||
|
||||
if (scrollTop + clientHeight >= scrollHeight - 120) {
|
||||
loadMoreFriends();
|
||||
}
|
||||
};
|
||||
|
||||
function loadMoreFriends() {
|
||||
if (isLoadingMore.value || itemsToShow.value >= filteredFriends.value.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingMore.value = true;
|
||||
|
||||
window.setTimeout(() => {
|
||||
if (itemsToShow.value < filteredFriends.value.length) {
|
||||
itemsToShow.value = Math.min(itemsToShow.value + PAGE_SIZE, filteredFriends.value.length);
|
||||
}
|
||||
isLoadingMore.value = false;
|
||||
maybeFillViewport();
|
||||
}, 350);
|
||||
scrollViewportRef.value = rootEl.querySelector('[data-slot="scroll-area-viewport"]');
|
||||
}
|
||||
|
||||
function maybeFillViewport() {
|
||||
nextTick(() => {
|
||||
const wrap = scrollbarRef.value?.viewportEl?.value;
|
||||
if (!wrap) {
|
||||
return;
|
||||
}
|
||||
const maxColumns = computed(() => {
|
||||
const styleFn = gridStyle.value;
|
||||
if (typeof styleFn !== 'function') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const { scrollHeight, clientHeight } = wrap;
|
||||
const hasSpace = scrollHeight <= clientHeight + VIEWPORT_BUFFER;
|
||||
const containerWidth = Math.max(gridWidth.value ?? 0, 0);
|
||||
|
||||
if (!hasSpace || isLoadingMore.value) {
|
||||
return;
|
||||
}
|
||||
const baseWidth = 220;
|
||||
const baseGap = 14;
|
||||
const scale = cardScale.value;
|
||||
const spacing = cardSpacing.value;
|
||||
const minWidth = baseWidth * scale;
|
||||
const gap = Math.max(6, (baseGap + (scale - 1) * 10) * spacing);
|
||||
|
||||
if (filteredFriends.value.length > visibleFriends.value.length) {
|
||||
loadMoreFriends();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
watch([searchTerm, activeSegment], () => {
|
||||
itemsToShow.value = PAGE_SIZE;
|
||||
nextTick(() => {
|
||||
updateGridWidth();
|
||||
maybeFillViewport();
|
||||
});
|
||||
return Math.max(1, Math.floor((containerWidth + gap) / (minWidth + gap)) || 1);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => filteredFriends.value.length,
|
||||
(length) => {
|
||||
if (itemsToShow.value > length) {
|
||||
itemsToShow.value = length;
|
||||
const chunk = (items = [], size = 1) => {
|
||||
const out = [];
|
||||
const n = Math.max(1, Math.floor(size) || 1);
|
||||
for (let i = 0; i < items.length; i += n) {
|
||||
out.push(items.slice(i, i + n));
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
const virtualRows = computed(() => {
|
||||
const rows = [];
|
||||
const columns = maxColumns.value;
|
||||
|
||||
if (isSameInstanceView.value) {
|
||||
for (const group of sameInstanceGroupsForVirtual.value) {
|
||||
rows.push({
|
||||
type: 'header',
|
||||
key: `h:${group.instanceId}`,
|
||||
instanceId: group.instanceId,
|
||||
count: Array.isArray(group.friends) ? group.friends.length : 0
|
||||
});
|
||||
|
||||
const friends = Array.isArray(group.friends) ? group.friends : [];
|
||||
for (const rowFriends of chunk(friends, Math.min(columns, friends.length || 1))) {
|
||||
rows.push({
|
||||
type: 'cards',
|
||||
key: `g:${group.instanceId}:${rowFriends
|
||||
.map((f) => f?.id ?? f?.userId ?? f?.displayName ?? '')
|
||||
.join('|')}`,
|
||||
items: rowFriends.map((friend) => ({
|
||||
key: `f:${friend?.id ?? friend?.userId ?? friend?.displayName ?? Math.random()}`,
|
||||
friend,
|
||||
displayInstanceInfo: true
|
||||
}))
|
||||
});
|
||||
}
|
||||
}
|
||||
nextTick(() => {
|
||||
updateGridWidth();
|
||||
maybeFillViewport();
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
if (shouldMergeSameInstance.value) {
|
||||
for (const group of mergedSameInstanceGroups.value) {
|
||||
rows.push({
|
||||
type: 'header',
|
||||
key: `h:${group.instanceId}`,
|
||||
instanceId: group.instanceId,
|
||||
count: Array.isArray(group.friends) ? group.friends.length : 0
|
||||
});
|
||||
|
||||
const friends = Array.isArray(group.friends) ? group.friends : [];
|
||||
for (const rowFriends of chunk(friends, Math.min(columns, friends.length || 1))) {
|
||||
rows.push({
|
||||
type: 'cards',
|
||||
key: `mg:${group.instanceId}:${rowFriends
|
||||
.map((f) => f?.id ?? f?.userId ?? f?.displayName ?? '')
|
||||
.join('|')}`,
|
||||
items: rowFriends.map((friend) => ({
|
||||
key: `f:${friend?.id ?? friend?.userId ?? friend?.displayName ?? Math.random()}`,
|
||||
friend,
|
||||
displayInstanceInfo: false
|
||||
}))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (mergedSameInstanceGroups.value.length && mergedOnlineEntries.value.length) {
|
||||
rows.push({ type: 'divider', key: 'divider:merged' });
|
||||
}
|
||||
|
||||
const online = mergedOnlineEntries.value;
|
||||
for (const rowEntries of chunk(online, Math.min(columns, online.length || 1))) {
|
||||
rows.push({
|
||||
type: 'cards',
|
||||
key: `o:${rowEntries.map((e) => e?.id ?? '').join('|')}`,
|
||||
items: rowEntries.map((entry) => ({
|
||||
key: `e:${entry?.id ?? entry?.friend?.id ?? entry?.friend?.displayName ?? Math.random()}`,
|
||||
friend: entry.friend,
|
||||
displayInstanceInfo: true
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
const entries = filteredFriends.value;
|
||||
for (const rowEntries of chunk(entries, Math.min(columns, entries.length || 1))) {
|
||||
rows.push({
|
||||
type: 'cards',
|
||||
key: `r:${rowEntries.map((e) => e?.id ?? '').join('|')}`,
|
||||
items: rowEntries.map((entry) => ({
|
||||
key: `e:${entry?.id ?? entry?.friend?.id ?? entry?.friend?.displayName ?? Math.random()}`,
|
||||
friend: entry.friend,
|
||||
displayInstanceInfo: true
|
||||
}))
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
});
|
||||
|
||||
const estimatedRowHeight = computed(() => {
|
||||
const base = 148;
|
||||
return Math.max(64, Math.round(base * cardScale.value * cardSpacing.value));
|
||||
});
|
||||
|
||||
const virtualizerRef = useVirtualizer(
|
||||
computed(() => ({
|
||||
count: virtualRows.value.length,
|
||||
getScrollElement: () => scrollViewportRef.value,
|
||||
estimateSize: (index) => {
|
||||
const row = virtualRows.value[index];
|
||||
if (row?.type === 'header') return 34;
|
||||
if (row?.type === 'divider') return 18;
|
||||
return estimatedRowHeight.value;
|
||||
},
|
||||
overscan: 10
|
||||
}))
|
||||
);
|
||||
|
||||
watch([cardScale, cardSpacing], () => {
|
||||
if (!settingsReady.value) {
|
||||
const virtualizer = computed(() => virtualizerRef.value);
|
||||
const virtualItems = computed(() => virtualizer.value?.getVirtualItems?.() ?? []);
|
||||
|
||||
const virtualSpacerStyle = computed(() => {
|
||||
const height = `${virtualizer.value?.getTotalSize?.() ?? 0}px`;
|
||||
return `height:${height};position:relative;width:100%;`;
|
||||
});
|
||||
|
||||
function virtualRowStyle(start) {
|
||||
const y = Number(start) || 0;
|
||||
return `transform:translateY(${y}px);position:absolute;top:0;left:0;width:100%;`;
|
||||
}
|
||||
|
||||
function onVirtualRowRef(el) {
|
||||
const target = el?.$el ?? el;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
virtualizer.value?.measureElement?.(/** @type {Element} */ (target));
|
||||
}
|
||||
|
||||
const virtualListStyle = computed(() => {
|
||||
const styleFn = gridStyle.value;
|
||||
const total = filteredFriends.value.length;
|
||||
|
||||
// Use matchMaxColumnWidth so rows don't collapse to minWidth on short rows.
|
||||
const vars = typeof styleFn === 'function' ? styleFn(total, { matchMaxColumnWidth: true }) : {};
|
||||
return {
|
||||
...vars
|
||||
};
|
||||
});
|
||||
|
||||
watch([searchTerm, activeSegment], () => {
|
||||
virtualizer.value?.scrollToOffset?.(0);
|
||||
nextTick(() => {
|
||||
resolveScrollViewport();
|
||||
updateGridWidth();
|
||||
maybeFillViewport();
|
||||
virtualizer.value?.measure?.();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -648,18 +666,41 @@
|
||||
activeSegment.value = 'online';
|
||||
}
|
||||
|
||||
itemsToShow.value = PAGE_SIZE;
|
||||
virtualizer.value?.scrollToOffset?.(0);
|
||||
nextTick(() => {
|
||||
resolveScrollViewport();
|
||||
updateGridWidth();
|
||||
virtualizer.value?.measure?.();
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
() => filteredFriends.value.length,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
resolveScrollViewport();
|
||||
updateGridWidth();
|
||||
virtualizer.value?.measure?.();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
watch([cardScale, cardSpacing], () => {
|
||||
if (!settingsReady.value) {
|
||||
return;
|
||||
}
|
||||
nextTick(() => {
|
||||
updateGridWidth();
|
||||
maybeFillViewport();
|
||||
virtualizer.value?.measure?.();
|
||||
});
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
resolveScrollViewport();
|
||||
setupResizeHandling();
|
||||
maybeFillViewport();
|
||||
updateGridWidth();
|
||||
virtualizer.value?.measure?.();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -696,9 +737,10 @@
|
||||
} finally {
|
||||
settingsReady.value = true;
|
||||
nextTick(() => {
|
||||
resolveScrollViewport();
|
||||
setupResizeHandling();
|
||||
updateGridWidth();
|
||||
maybeFillViewport();
|
||||
virtualizer.value?.measure?.();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -747,6 +789,30 @@
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.friend-view__virtual {
|
||||
width: 100%;
|
||||
padding: 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.friend-view__virtual-spacer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.friend-view__virtual-row {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.friend-view__row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: var(--friend-card-gap, 14px);
|
||||
align-items: stretch;
|
||||
padding: 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.friend-view__settings-label {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
|
||||
Reference in New Issue
Block a user