Added message-to-activity conversion and edited the colleague selection modal.
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import axios from 'axios';
|
||||
import { api } from '@/lib/api';
|
||||
import type {
|
||||
ActivityDto,
|
||||
ActivityType,
|
||||
ConversationAccessDto,
|
||||
ConversationPeerDto,
|
||||
ConversationPinnedMessageDto,
|
||||
ConversationRoomDto,
|
||||
CreateActivityRequest,
|
||||
} from '@mento/shared';
|
||||
import { formatClockFa, toFaDigits } from '@/lib/jalali';
|
||||
import {
|
||||
@@ -20,6 +24,7 @@ import {
|
||||
type MessagePinEvent,
|
||||
type RoomLiveEvent,
|
||||
} from '@/lib/useOrgChat';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import UserAvatar from '@/components/UserAvatar.vue';
|
||||
|
||||
@@ -31,6 +36,8 @@ type PendingAttach = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
const access = ref<ConversationAccessDto | null>(null);
|
||||
const rooms = ref<ConversationRoomDto[]>([]);
|
||||
const peers = ref<ConversationPeerDto[]>([]);
|
||||
@@ -46,6 +53,7 @@ const groupMemberIds = ref<string[]>([]);
|
||||
const channelName = ref('');
|
||||
const channelMemberIds = ref<string[]>([]);
|
||||
const dmPeerId = ref('');
|
||||
const peerPickerQuery = ref('');
|
||||
const chatUserId = ref('');
|
||||
const pendingAttaches = ref<PendingAttach[]>([]);
|
||||
const sending = ref(false);
|
||||
@@ -106,6 +114,20 @@ const chatSearchQuery = ref('');
|
||||
const chatSearchIndex = ref(0);
|
||||
const mentionState = ref<{ start: number; end: number; query: string } | null>(null);
|
||||
const mentionIndex = ref(0);
|
||||
const showActivityFromMsg = ref(false);
|
||||
const activityFromMsg = ref<ChatMessage | null>(null);
|
||||
const activityTitle = ref('');
|
||||
const activityType = ref<ActivityType>('task');
|
||||
const activityBusy = ref(false);
|
||||
const activityError = ref('');
|
||||
const activityCreatedId = ref('');
|
||||
|
||||
const ACTIVITY_TYPE_OPTS: Array<{ value: ActivityType; label: string }> = [
|
||||
{ value: 'task', label: 'وظیفه' },
|
||||
{ value: 'call', label: 'تماس' },
|
||||
{ value: 'meeting', label: 'جلسه' },
|
||||
{ value: 'note', label: 'یادداشت' },
|
||||
];
|
||||
|
||||
const canCreateGroup = computed(() => access.value?.canCreateGroup ?? false);
|
||||
const canCreateChannel = computed(
|
||||
@@ -118,6 +140,19 @@ const canPostInActive = computed(() => {
|
||||
return room.canPost !== false;
|
||||
});
|
||||
|
||||
const filteredPeers = computed(() => {
|
||||
const q = peerPickerQuery.value.trim().toLowerCase();
|
||||
if (!q) return peers.value;
|
||||
return peers.value.filter((p) => p.fullName.toLowerCase().includes(q));
|
||||
});
|
||||
|
||||
const selectedGroupPeers = computed(() =>
|
||||
peers.value.filter((p) => groupMemberIds.value.includes(p.id)),
|
||||
);
|
||||
const selectedChannelPeers = computed(() =>
|
||||
peers.value.filter((p) => channelMemberIds.value.includes(p.id)),
|
||||
);
|
||||
|
||||
async function scrollToBottom() {
|
||||
await nextTick();
|
||||
const el = msgStreamEl.value;
|
||||
@@ -275,6 +310,7 @@ function closeMobileChat() {
|
||||
closeChatSearch();
|
||||
mentionState.value = null;
|
||||
pinnedMessage.value = null;
|
||||
closeActivityFromMsg();
|
||||
if (recording.value) mediaRecorder?.stop();
|
||||
}
|
||||
|
||||
@@ -1094,6 +1130,112 @@ function toggleChannelMember(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function openNewDm() {
|
||||
peerPickerQuery.value = '';
|
||||
dmPeerId.value = '';
|
||||
showNewDm.value = true;
|
||||
}
|
||||
|
||||
function openNewGroup() {
|
||||
peerPickerQuery.value = '';
|
||||
groupName.value = '';
|
||||
groupMemberIds.value = [];
|
||||
showNewGroup.value = true;
|
||||
}
|
||||
|
||||
function openNewChannel() {
|
||||
peerPickerQuery.value = '';
|
||||
channelName.value = '';
|
||||
channelMemberIds.value = [];
|
||||
showNewChannel.value = true;
|
||||
}
|
||||
|
||||
function messageToActivityTitle(msg: ChatMessage) {
|
||||
if (msg.type === 'text') {
|
||||
const t = (msg.content || '').trim().replace(/\s+/g, ' ');
|
||||
if (!t) return 'پیگیری گفتگو';
|
||||
return t.length > 80 ? `${t.slice(0, 80)}…` : t;
|
||||
}
|
||||
return `پیگیری: ${previewLabelForMessage(msg)}`;
|
||||
}
|
||||
|
||||
function messageToActivityDescription(msg: ChatMessage) {
|
||||
const roomName = activeRoom.value ? roomTitle(activeRoom.value) : 'گفتگو';
|
||||
const body =
|
||||
msg.type === 'text'
|
||||
? (msg.content || '').trim()
|
||||
: previewLabelForMessage(msg);
|
||||
return `ایجادشده از گفتگو «${roomName}»\n\n${body}`.trim();
|
||||
}
|
||||
|
||||
function openConvertToActivity(msg: ChatMessage) {
|
||||
if (msg.deleted || msg.optimistic) return;
|
||||
msgMenu.value = null;
|
||||
activityFromMsg.value = msg;
|
||||
activityTitle.value = messageToActivityTitle(msg);
|
||||
activityType.value = 'task';
|
||||
activityError.value = '';
|
||||
activityCreatedId.value = '';
|
||||
showActivityFromMsg.value = true;
|
||||
}
|
||||
|
||||
function closeActivityFromMsg() {
|
||||
showActivityFromMsg.value = false;
|
||||
activityFromMsg.value = null;
|
||||
activityBusy.value = false;
|
||||
activityError.value = '';
|
||||
activityCreatedId.value = '';
|
||||
}
|
||||
|
||||
async function submitActivityFromMsg() {
|
||||
const msg = activityFromMsg.value;
|
||||
const title = activityTitle.value.trim();
|
||||
if (!msg || !title) {
|
||||
activityError.value = 'عنوان فعالیت را وارد کنید.';
|
||||
return;
|
||||
}
|
||||
activityBusy.value = true;
|
||||
activityError.value = '';
|
||||
try {
|
||||
const payload: CreateActivityRequest = {
|
||||
title,
|
||||
type: activityType.value,
|
||||
description: messageToActivityDescription(msg),
|
||||
assigneeId: auth.user?.id || undefined,
|
||||
tags: ['گفتگو'],
|
||||
};
|
||||
const { data } = await api.post<ActivityDto>('/activities', payload);
|
||||
activityCreatedId.value = data.id;
|
||||
} catch (e) {
|
||||
if (axios.isAxiosError(e)) {
|
||||
const msgBody = (e.response?.data as { message?: string | string[] })?.message;
|
||||
activityError.value = Array.isArray(msgBody)
|
||||
? msgBody.join('، ')
|
||||
: msgBody || 'ساخت فعالیت ناموفق بود.';
|
||||
} else {
|
||||
activityError.value = 'ساخت فعالیت ناموفق بود.';
|
||||
}
|
||||
} finally {
|
||||
activityBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openCreatedActivity() {
|
||||
const id = activityCreatedId.value;
|
||||
closeActivityFromMsg();
|
||||
if (id) await router.push({ name: 'activity-detail', params: { id } });
|
||||
}
|
||||
|
||||
watch(showNewDm, (v) => {
|
||||
if (v) peerPickerQuery.value = '';
|
||||
});
|
||||
watch(showNewGroup, (v) => {
|
||||
if (v) peerPickerQuery.value = '';
|
||||
});
|
||||
watch(showNewChannel, (v) => {
|
||||
if (v) peerPickerQuery.value = '';
|
||||
});
|
||||
|
||||
watch(activeRoom, () => {
|
||||
draft.value = '';
|
||||
clearPendingAttaches();
|
||||
@@ -1177,12 +1319,12 @@ onUnmounted(() => {
|
||||
<div v-else class="conv-shell">
|
||||
<aside class="conv-rail" :hidden="mobileChatOpen">
|
||||
<div class="rail-actions">
|
||||
<button type="button" class="fab" @click="showNewDm = true">+ گفتگو</button>
|
||||
<button type="button" class="fab" @click="openNewDm">+ گفتگو</button>
|
||||
<button
|
||||
v-if="canCreateGroup"
|
||||
type="button"
|
||||
class="fab ghost"
|
||||
@click="showNewGroup = true"
|
||||
@click="openNewGroup"
|
||||
>
|
||||
گروه
|
||||
</button>
|
||||
@@ -1190,7 +1332,7 @@ onUnmounted(() => {
|
||||
v-if="canCreateChannel"
|
||||
type="button"
|
||||
class="fab ghost"
|
||||
@click="showNewChannel = true"
|
||||
@click="openNewChannel"
|
||||
>
|
||||
کانال
|
||||
</button>
|
||||
@@ -1706,6 +1848,10 @@ onUnmounted(() => {
|
||||
<UiIcon name="send" :size="16" />
|
||||
ارسال برای دیگری
|
||||
</button>
|
||||
<button type="button" @click="openConvertToActivity(menuMessage()!)">
|
||||
<UiIcon name="check" :size="16" />
|
||||
تبدیل به فعالیت
|
||||
</button>
|
||||
<button
|
||||
v-if="isMineMessage(menuMessage()) && menuMessage()?.type === 'text'"
|
||||
type="button"
|
||||
@@ -1739,15 +1885,41 @@ onUnmounted(() => {
|
||||
</div>
|
||||
|
||||
<dialog v-if="showNewDm" class="modal" open @click.self="showNewDm = false">
|
||||
<div class="modal-card">
|
||||
<div class="modal-card wide">
|
||||
<h3>گفتگوی جدید</h3>
|
||||
<select v-model="dmPeerId">
|
||||
<option value="">همکار را انتخاب کنید</option>
|
||||
<option v-for="p in peers" :key="p.id" :value="p.id">{{ p.fullName }}</option>
|
||||
</select>
|
||||
<p class="hint">همکار موردنظر را انتخاب کنید</p>
|
||||
<label class="peer-search">
|
||||
<UiIcon name="search" :size="16" />
|
||||
<input
|
||||
v-model="peerPickerQuery"
|
||||
type="search"
|
||||
placeholder="جستجوی نام…"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</label>
|
||||
<div class="peer-list">
|
||||
<button
|
||||
v-for="p in filteredPeers"
|
||||
:key="p.id"
|
||||
type="button"
|
||||
class="peer-row"
|
||||
:class="{ on: dmPeerId === p.id }"
|
||||
@click="dmPeerId = p.id"
|
||||
>
|
||||
<UserAvatar :name="p.fullName" :size="40" />
|
||||
<span class="peer-row-meta">
|
||||
<strong>{{ p.fullName }}</strong>
|
||||
<small>گفتگوی خصوصی</small>
|
||||
</span>
|
||||
<span class="peer-tick" aria-hidden="true">
|
||||
<UiIcon v-if="dmPeerId === p.id" name="check-plain" :size="14" />
|
||||
</span>
|
||||
</button>
|
||||
<p v-if="!filteredPeers.length" class="peer-empty">همکاری پیدا نشد</p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" @click="showNewDm = false">انصراف</button>
|
||||
<button type="button" class="primary" @click="startDm">شروع</button>
|
||||
<button type="button" class="primary" :disabled="!dmPeerId" @click="startDm">شروع</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -1756,20 +1928,65 @@ onUnmounted(() => {
|
||||
<div class="modal-card wide">
|
||||
<h3>گروه جدید</h3>
|
||||
<input v-model="groupName" type="text" placeholder="نام گروه" />
|
||||
<p class="hint">اعضا</p>
|
||||
<div class="member-grid">
|
||||
<label v-for="p in peers" :key="p.id" class="member-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="groupMemberIds.includes(p.id)"
|
||||
@change="toggleGroupMember(p.id)"
|
||||
/>
|
||||
{{ p.fullName }}
|
||||
</label>
|
||||
<div class="picker-head">
|
||||
<p class="hint">اعضا را انتخاب کنید</p>
|
||||
<span v-if="groupMemberIds.length" class="picker-count">
|
||||
{{ toFaDigits(groupMemberIds.length) }} نفر
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="selectedGroupPeers.length" class="picked-chips">
|
||||
<button
|
||||
v-for="p in selectedGroupPeers"
|
||||
:key="p.id"
|
||||
type="button"
|
||||
class="picked-chip"
|
||||
:title="`حذف ${p.fullName}`"
|
||||
@click="toggleGroupMember(p.id)"
|
||||
>
|
||||
<UserAvatar :name="p.fullName" :size="22" />
|
||||
<span>{{ p.fullName }}</span>
|
||||
<UiIcon name="x" :size="12" />
|
||||
</button>
|
||||
</div>
|
||||
<label class="peer-search">
|
||||
<UiIcon name="search" :size="16" />
|
||||
<input
|
||||
v-model="peerPickerQuery"
|
||||
type="search"
|
||||
placeholder="جستجوی همکار…"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</label>
|
||||
<div class="peer-list">
|
||||
<button
|
||||
v-for="p in filteredPeers"
|
||||
:key="p.id"
|
||||
type="button"
|
||||
class="peer-row"
|
||||
:class="{ on: groupMemberIds.includes(p.id) }"
|
||||
@click="toggleGroupMember(p.id)"
|
||||
>
|
||||
<UserAvatar :name="p.fullName" :size="40" />
|
||||
<span class="peer-row-meta">
|
||||
<strong>{{ p.fullName }}</strong>
|
||||
<small>{{ groupMemberIds.includes(p.id) ? 'انتخابشده' : 'برای افزودن بزنید' }}</small>
|
||||
</span>
|
||||
<span class="peer-tick" aria-hidden="true">
|
||||
<UiIcon v-if="groupMemberIds.includes(p.id)" name="check-plain" :size="14" />
|
||||
</span>
|
||||
</button>
|
||||
<p v-if="!filteredPeers.length" class="peer-empty">همکاری پیدا نشد</p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" @click="showNewGroup = false">انصراف</button>
|
||||
<button type="button" class="primary" @click="startGroup">ساخت گروه</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
:disabled="!groupName.trim() || !groupMemberIds.length"
|
||||
@click="startGroup"
|
||||
>
|
||||
ساخت گروه
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -1777,26 +1994,124 @@ onUnmounted(() => {
|
||||
<dialog v-if="showNewChannel" class="modal" open @click.self="showNewChannel = false">
|
||||
<div class="modal-card wide">
|
||||
<h3>کانال جدید</h3>
|
||||
<p class="hint">کانال یکطرفه است؛ فقط شما و مدیران سازمان میتوانند پیام بفرستند.</p>
|
||||
<p class="hint channel-hint">
|
||||
کانال یکطرفه است؛ فقط شما و مدیران سازمان میتوانند پیام بفرستند.
|
||||
</p>
|
||||
<input v-model="channelName" type="text" placeholder="نام کانال" />
|
||||
<p class="hint">اعضا (مشاهدهکنندگان)</p>
|
||||
<div class="member-grid">
|
||||
<label v-for="p in peers" :key="p.id" class="member-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="channelMemberIds.includes(p.id)"
|
||||
@change="toggleChannelMember(p.id)"
|
||||
/>
|
||||
{{ p.fullName }}
|
||||
</label>
|
||||
<div class="picker-head">
|
||||
<p class="hint">مشاهدهکنندگان</p>
|
||||
<span v-if="channelMemberIds.length" class="picker-count">
|
||||
{{ toFaDigits(channelMemberIds.length) }} نفر
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="selectedChannelPeers.length" class="picked-chips">
|
||||
<button
|
||||
v-for="p in selectedChannelPeers"
|
||||
:key="p.id"
|
||||
type="button"
|
||||
class="picked-chip"
|
||||
:title="`حذف ${p.fullName}`"
|
||||
@click="toggleChannelMember(p.id)"
|
||||
>
|
||||
<UserAvatar :name="p.fullName" :size="22" />
|
||||
<span>{{ p.fullName }}</span>
|
||||
<UiIcon name="x" :size="12" />
|
||||
</button>
|
||||
</div>
|
||||
<label class="peer-search">
|
||||
<UiIcon name="search" :size="16" />
|
||||
<input
|
||||
v-model="peerPickerQuery"
|
||||
type="search"
|
||||
placeholder="جستجوی همکار…"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</label>
|
||||
<div class="peer-list">
|
||||
<button
|
||||
v-for="p in filteredPeers"
|
||||
:key="p.id"
|
||||
type="button"
|
||||
class="peer-row"
|
||||
:class="{ on: channelMemberIds.includes(p.id) }"
|
||||
@click="toggleChannelMember(p.id)"
|
||||
>
|
||||
<UserAvatar :name="p.fullName" :size="40" />
|
||||
<span class="peer-row-meta">
|
||||
<strong>{{ p.fullName }}</strong>
|
||||
<small>{{ channelMemberIds.includes(p.id) ? 'انتخابشده' : 'برای افزودن بزنید' }}</small>
|
||||
</span>
|
||||
<span class="peer-tick" aria-hidden="true">
|
||||
<UiIcon v-if="channelMemberIds.includes(p.id)" name="check-plain" :size="14" />
|
||||
</span>
|
||||
</button>
|
||||
<p v-if="!filteredPeers.length" class="peer-empty">همکاری پیدا نشد</p>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button type="button" @click="showNewChannel = false">انصراف</button>
|
||||
<button type="button" class="primary" @click="startChannel">ساخت کانال</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
:disabled="!channelName.trim() || !channelMemberIds.length"
|
||||
@click="startChannel"
|
||||
>
|
||||
ساخت کانال
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog
|
||||
v-if="showActivityFromMsg"
|
||||
class="modal"
|
||||
open
|
||||
@click.self="closeActivityFromMsg"
|
||||
>
|
||||
<div class="modal-card activity-sheet">
|
||||
<template v-if="!activityCreatedId">
|
||||
<h3>تبدیل به فعالیت</h3>
|
||||
<p class="hint">از این پیام یک فعالیت در CRM ساخته میشود.</p>
|
||||
<label class="field-label">عنوان</label>
|
||||
<input v-model="activityTitle" type="text" placeholder="عنوان فعالیت" />
|
||||
<label class="field-label">نوع</label>
|
||||
<div class="type-chips">
|
||||
<button
|
||||
v-for="opt in ACTIVITY_TYPE_OPTS"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
class="type-chip"
|
||||
:class="{ on: activityType === opt.value }"
|
||||
@click="activityType = opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="activityError" class="act-err">{{ activityError }}</p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" :disabled="activityBusy" @click="closeActivityFromMsg">
|
||||
انصراف
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="primary"
|
||||
:disabled="activityBusy || !activityTitle.trim()"
|
||||
@click="submitActivityFromMsg"
|
||||
>
|
||||
{{ activityBusy ? '…' : 'ساخت فعالیت' }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<h3>فعالیت ساخته شد</h3>
|
||||
<p class="hint">میتوانید جزئیات را در صفحه فعالیت کامل کنید.</p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" @click="closeActivityFromMsg">بستن</button>
|
||||
<button type="button" class="primary" @click="openCreatedActivity">مشاهده فعالیت</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog v-if="showForward" class="modal" open @click.self="showForward = false">
|
||||
<div class="modal-card">
|
||||
<h3>ارسال برای دیگری</h3>
|
||||
@@ -2926,40 +3241,219 @@ onUnmounted(() => {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
.modal-card.wide {
|
||||
width: min(480px, 100%);
|
||||
}
|
||||
.modal-card select,
|
||||
.modal-card input {
|
||||
width: 100%;
|
||||
padding: 0.55rem 0.65rem;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--line);
|
||||
font: inherit;
|
||||
}
|
||||
.member-grid {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
max-height: 200px;
|
||||
width: min(440px, 100%);
|
||||
max-height: min(88vh, 640px);
|
||||
overflow: auto;
|
||||
}
|
||||
.member-check {
|
||||
.modal-card select,
|
||||
.modal-card input[type='text'],
|
||||
.modal-card input[type='search'],
|
||||
.modal-card input:not([type]) {
|
||||
width: 100%;
|
||||
padding: 0.6rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--line);
|
||||
font: inherit;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.picker-head {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.picker-head .hint {
|
||||
margin: 0;
|
||||
}
|
||||
.picker-count {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
color: #0f766e;
|
||||
background: rgba(13, 148, 136, 0.12);
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.channel-hint {
|
||||
margin: 0;
|
||||
padding: 0.55rem 0.7rem;
|
||||
border-radius: 12px;
|
||||
background: #fff7ed;
|
||||
color: #9a3412;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.peer-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
padding: 0.45rem 0.7rem;
|
||||
border-radius: 12px;
|
||||
background: rgba(148, 163, 184, 0.12);
|
||||
border: 1px solid rgba(148, 163, 184, 0.22);
|
||||
color: var(--muted);
|
||||
}
|
||||
.peer-search input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
padding: 0.15rem 0 !important;
|
||||
outline: none;
|
||||
font: inherit;
|
||||
color: var(--text);
|
||||
}
|
||||
.picked-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.picked-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
border: 1px solid rgba(13, 148, 136, 0.28);
|
||||
background: rgba(13, 148, 136, 0.08);
|
||||
color: #0f766e;
|
||||
border-radius: 999px;
|
||||
padding: 0.2rem 0.45rem 0.2rem 0.25rem;
|
||||
font: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
max-width: 100%;
|
||||
}
|
||||
.picked-chip span {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 9rem;
|
||||
}
|
||||
.peer-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
max-height: 240px;
|
||||
overflow: auto;
|
||||
padding: 0.15rem;
|
||||
margin: 0 -0.15rem;
|
||||
}
|
||||
.peer-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
background: #fff;
|
||||
border-radius: 14px;
|
||||
padding: 0.45rem 0.55rem;
|
||||
text-align: right;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
color: var(--text);
|
||||
transition: background 0.12s ease, border-color 0.12s ease;
|
||||
}
|
||||
.peer-row:hover {
|
||||
background: rgba(13, 148, 136, 0.05);
|
||||
}
|
||||
.peer-row.on {
|
||||
background: rgba(13, 148, 136, 0.1);
|
||||
border-color: rgba(13, 148, 136, 0.35);
|
||||
}
|
||||
.peer-row-meta {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
.peer-row-meta strong {
|
||||
font-size: 0.92rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.peer-row-meta small {
|
||||
color: var(--muted);
|
||||
font-size: 0.74rem;
|
||||
}
|
||||
.peer-tick {
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
border-radius: 999px;
|
||||
border: 1.5px solid rgba(148, 163, 184, 0.55);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.peer-row.on .peer-tick {
|
||||
background: #0d9488;
|
||||
border-color: #0d9488;
|
||||
}
|
||||
.peer-empty {
|
||||
margin: 0.75rem 0;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.field-label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
.type-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.type-chip {
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
border-radius: 999px;
|
||||
padding: 0.35rem 0.75rem;
|
||||
font: inherit;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
.type-chip.on {
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
border-color: rgba(99, 102, 241, 0.45);
|
||||
color: #4338ca;
|
||||
}
|
||||
.act-err {
|
||||
margin: 0;
|
||||
color: #b91c1c;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.activity-sheet {
|
||||
width: min(400px, 100%);
|
||||
}
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
.modal-actions .primary {
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 0.45rem 0.85rem;
|
||||
.modal-actions button {
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
padding: 0.5rem 0.9rem;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
.modal-actions .primary {
|
||||
background: #0d9488;
|
||||
color: #fff;
|
||||
border-color: #0d9488;
|
||||
}
|
||||
.modal-actions .primary:disabled,
|
||||
.modal-actions button:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
@@ -3102,6 +3596,14 @@ onUnmounted(() => {
|
||||
.composer {
|
||||
position: relative;
|
||||
}
|
||||
.activity-sheet,
|
||||
.modal-card.wide {
|
||||
width: min(100%, 100vw - 1.5rem);
|
||||
border-radius: 18px 18px 14px 14px;
|
||||
}
|
||||
.peer-list {
|
||||
max-height: 38vh;
|
||||
}
|
||||
.msg-stream {
|
||||
padding: 0.75rem 0.7rem 0.5rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user