diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..cb5f4d2 --- /dev/null +++ b/.env.development @@ -0,0 +1,3 @@ +VUE_APP_RUNTIME_ENV=local +VUE_APP_APP_SERVER_TARGET=http://18.141.44.165:8888 +VUE_APP_WEB_API_TARGET=http://18.141.44.165:9000 diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..b329275 --- /dev/null +++ b/.env.production @@ -0,0 +1 @@ +VUE_APP_RUNTIME_ENV=production diff --git a/.gitignore b/.gitignore index 240c3a3..4fd1d47 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* *.local -.env +.env .env.* -SESSION_HANDOFF.md +!.env.development +!.env.production diff --git a/SESSION_HANDOFF.md b/SESSION_HANDOFF.md new file mode 100644 index 0000000..de8929d --- /dev/null +++ b/SESSION_HANDOFF.md @@ -0,0 +1,299 @@ +# JC Web IM Session Handoff + +Updated: 2026-05-02 03:18:54 UTC + +## Current Goal + +Continue the Web IM version under: + +```text +/var/www/jc-im/jc-web-im +``` + +The current focus is the contact list and contact detail flow: + +- Load contacts quickly. +- Avoid duplicate contact rows. +- Show contact detail fields correctly. +- Allow editing the friend alias / remark name. + +Important user instruction: + +- Do not modify `app-server` code. +- Local config may be adjusted to fit the local test environment. +- Web-side code and PHP proxy code may be changed as needed. + +## Runtime Services + +Current local runtime: + +```text +im-server: /opt/im-server +app-server: /opt/app-server +web source: /var/www/jc-im/jc-web-im +web dist: /var/www/jc-im/jc-web-im/dist +``` + +PM2 process names: + +```text +jc-im-server +jc-app-server +``` + +Useful checks: + +```bash +pm2 list +curl -s http://127.0.0.1:18000/api/version +curl -s http://127.0.0.1:18888/ +curl -I http://127.0.0.1:8023/ +``` + +Important ports: + +```text +18000 im-server HTTP/API +18080 im-server admin +1883 im-server long connection/MQTT +18888 app-server +8023 Web IM Nginx +``` + +## App Server Status + +Remote `app-server` was previously pulled from: + +```text +ubuntu@18.138.246.10:/opt/app-server +``` + +It is a colleague-modified deployment and is no longer started from a jar. It runs from exploded classes/lib, equivalent to: + +```bash +java -Dfile.encoding=UTF-8 -Xms256m -Xmx1024m \ + -classpath classes:lib/* cn.wildfirechat.app.Application +``` + +Do not change its code unless the user explicitly reverses that instruction. + +Current local config was preserved/adapted under: + +```text +/opt/app-server/config/application.properties +``` + +Known important setting: + +```properties +AppCode=676d5adc3ee049b4aeb69f440305c953 +``` + +Redis was configured locally to accept the password expected by this app-server: + +```text +Mjfg2U@YZdLx&wmH +``` + +Be careful: other Redis clients may need auth if they reconnect. + +## Contact List Architecture + +The Web version currently uses PHP proxy endpoints under: + +```text +/var/www/jc-im/jc-web-im/public/api +``` + +Build copies them to: + +```text +/var/www/jc-im/jc-web-im/dist/api +``` + +Current PHP APIs: + +```text +contacts.php +update_alias.php +add_contact.php +send_message.php +messages.php +im_web_bootstrap.php +``` + +The PHP proxy exists because the browser must not directly carry the `im-server` admin secret. + +Admin config is in: + +```text +public/api/im_web_bootstrap.php +``` + +Current values: + +```php +const IM_ADMIN_URL = 'http://127.0.0.1:18080'; +const IM_ADMIN_SECRET = '123456'; +``` + +### Contact List API + +Endpoint: + +```text +POST /api/contacts.php +``` + +Payload: + +```json +{"userId":"当前登录用户ID"} +``` + +Current logic: + +1. Calls `im-server` admin `/admin/friend/list`. +2. Calls `/admin/user/batch_get_infos`. +3. Calls `/admin/friend/get_alias` for each friend. +4. Returns unique contacts with `uid`, `displayName`, `mobile`, `portrait`, `address`, `alias`. + +This was verified to be fast locally, around milliseconds rather than the old 8-10 seconds. + +### Remark / Alias Update API + +Endpoint: + +```text +POST /api/update_alias.php +``` + +Payload: + +```json +{"userId":"当前登录用户ID","targetId":"联系人ID","alias":"备注名"} +``` + +Current logic: + +1. Validates `userId`, `targetId`. +2. Allows empty alias to clear remark. +3. Limits alias to 32 UTF-8 characters. +4. Calls `im-server` admin `/admin/friend/set_alias`. + +Verified manually: + +- Setting alias to `测试备注` succeeded. +- `contacts.php` returned the new alias. +- Test alias was then cleared back to empty. + +## Frontend Changes Completed + +Files touched for contact details and alias editing: + +```text +src/config.js +src/services/httpImClient.js +src/stores/contacts.js +src/components/contacts/ContactPanel.vue +src/styles/base.css +public/api/update_alias.php +``` + +Important behavior now: + +- `contactsApi` defaults to `/api/contacts.php` in production. +- `updateAliasApi` defaults to `/api/update_alias.php` in production. +- `HttpImClient.getServerContactList()` maps `address` and `alias`. +- `HttpImClient.updateContactAlias()` posts to the PHP proxy. +- `contacts.updateContactAlias()` updates local contact state immediately after successful save. +- Contact detail display is: + - avatar / portrait + - remark name from `t_friend._alias`; if empty, show `未设置` + - username from `_display_name` + - mobile from `_mobile` + - address from `_address`; if empty, show `未知` +- A `✏️` button appears next to the remark name. Clicking it opens a modal to edit the remark. + +## Build / Deploy Status + +Last successful build: + +```bash +cd /var/www/jc-im/jc-web-im +npm run build +``` + +Build output included: + +```text +dist/js/app.28165923.js +dist/css/app.08b643a5.css +dist/api/update_alias.php +``` + +Build warnings were only webpack asset-size warnings for `chunk-vendors`, not functional errors. + +Syntax checks passed: + +```bash +php -l jc-web-im/public/api/update_alias.php +php -l jc-web-im/dist/api/update_alias.php +``` + +## Deployment Documentation + +Deployment doc has been added: + +```text +/var/www/jc-im/jc-web-im/WEB_DEPLOYMENT.md +``` + +README links to it: + +```text +/var/www/jc-im/jc-web-im/README.md +``` + +Key deployment note: + +- Production needs Nginx + PHP-FPM. +- PHP minimum recommended version: 7.4. +- PHP recommended versions: 8.1 / 8.2 / 8.3. +- Required PHP extensions: `curl`, `mbstring`. +- Nginx root should point to `/var/www/jc-im/jc-web-im/dist`. +- `18080` admin port should not be exposed publicly. + +## Known Current Test User Data + +Recent contact list test used: + +```text +current userId: 58f71e6245e145409e69b30216c6ebad +``` + +Contacts returned: + +```text +FireRobot +571356e9237c460698329c1653d6fd63 用户5050 183019202118 +55737875bf7a4d089102cbba03b1a8b8 用户8592 18301902118 +``` + +Both human contacts had empty alias after the manual test was restored. + +## Suggested Next Steps + +Good continuation points: + +1. Open the Web page on port `8023` and manually confirm the alias edit modal in the browser. +2. Check whether the contact header should also show the `✏️` edit control or only the profile card should show it. +3. Continue message send/sync polish if contact flow is accepted. +4. Consider optimizing `contacts.php` alias loading if a user has many friends, because it currently calls `get_alias` once per friend. + +## Do Not Forget + +- Do not modify `/opt/app-server` code. +- If changing PHP proxy admin secret or admin URL, update source under `public/api` and rebuild. +- If editing runtime config after build, check both `public/config/runtime-config.js` and `dist/config/runtime-config.js` depending on whether the change should survive future builds. +- The current workspace is `/var/www/jc-im`, not the old `/var/www/im-web`. diff --git a/WEB_DEPLOYMENT.md b/WEB_DEPLOYMENT.md index 43b9d39..ccf3727 100644 --- a/WEB_DEPLOYMENT.md +++ b/WEB_DEPLOYMENT.md @@ -50,27 +50,20 @@ im-server HTTP/API: http://127.0.0.1:18000 im-server admin: http://127.0.0.1:18080 ``` -`im-server admin`、IM 数据库和应用数据库配置通过 PHP-FPM 环境变量提供: +`im-server admin` 的地址和密钥在: ```text public/api/im_web_bootstrap.php ``` -需要配置: - -```text -IM_ADMIN_URL=http://127.0.0.1:18080 -IM_ADMIN_SECRET=请填写管理端密钥 -IM_DB_DSN=mysql:host=127.0.0.1;port=3306;dbname=wfchat;charset=utf8mb4 -IM_DB_USER=请填写数据库用户 -IM_DB_PASSWORD=请填写数据库密码 -IM_APP_DB_DSN=mysql:host=127.0.0.1;port=3306;dbname=appdata;charset=utf8mb4 -IM_APP_DB_USER=请填写应用数据库用户 -IM_APP_DB_PASSWORD=请填写应用数据库密码 -IM_DB_TIMEZONE=Asia/Bangkok -``` - -请在 PHP-FPM pool、容器 secret 或部署平台的环境变量中配置,禁止把真实密钥提交到仓库。 +默认配置: + +```php +const IM_ADMIN_URL = 'http://127.0.0.1:18080'; +const IM_ADMIN_SECRET = '123456'; +``` + +如果生产环境 admin 端口或 secret 不同,需要同步修改该文件后重新构建。 ## 构建 diff --git a/deploy/im-web-8023.nginx.conf b/deploy/im-web-8023.nginx.conf index 0f73a5b..749614d 100644 --- a/deploy/im-web-8023.nginx.conf +++ b/deploy/im-web-8023.nginx.conf @@ -4,8 +4,12 @@ server { server_name winchen.ezpay.asia 10.0.0.120 _; root /var/www/im-web; - index index.html; - client_max_body_size 100m; + index index.html; + client_max_body_size 100m; + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types application/json; location = / { try_files /index.html =404; @@ -13,12 +17,19 @@ server { add_header Cache-Control "no-store"; } - location = /index.html { + location = /index.html { expires -1; add_header Cache-Control "no-store"; - } - - location ~ ^/api/.+\.php$ { + } + + location = /api/media_proxy.php { + include snippets/fastcgi-php.conf; + fastcgi_param PHP_ADMIN_VALUE "open_basedir=/var/www/im-web/:/tmp/"; + fastcgi_pass unix:/run/php/php8.3-fpm.sock; + fastcgi_read_timeout 300; + } + + location ~ ^/api/.+\.php$ { include snippets/fastcgi-php.conf; fastcgi_param PHP_ADMIN_VALUE "open_basedir=/var/www/im-web/:/tmp/"; fastcgi_pass unix:/run/php/php8.3-fpm.sock; diff --git a/dist-local/api/add_contact.php b/dist-local/api/add_contact.php new file mode 100644 index 0000000..586f066 --- /dev/null +++ b/dist-local/api/add_contact.php @@ -0,0 +1,95 @@ + $uid, + 'name' => (string)($user['name'] ?? $uid), + 'displayName' => (string)($user['displayName'] ?? $user['name'] ?? $uid), + 'mobile' => (string)($user['mobile'] ?? ''), + 'portrait' => (string)($user['portrait'] ?? ''), + 'alias' => '', + ]; +} + +try { + $target = tryImUserInfo(['userId' => $keyword]); + if (!$target && preg_match('/^\+?\d{5,32}$/', $keyword)) { + $target = tryImUserInfo(['mobile' => $keyword]); + } + if (!$target) { + $target = tryImUserInfo(['name' => $keyword]); + } + + if (!$target) { + respond(404, '没有找到该用户'); + } + + $contact = normalizeUserInfo($target); + if ($contact['uid'] === '') { + respond(404, '没有找到该用户'); + } + if ($contact['uid'] === $userId) { + respond(400, '不能添加自己为联系人'); + } + + $friendResult = requireImSuccess( + imAdminPost('/admin/friend/list', ['userId' => $userId]), + '好友列表检查失败' + ); + $friendIds = array_values(array_filter($friendResult['list'] ?? [], 'is_string')); + if (in_array($contact['uid'], $friendIds, true)) { + respond(409, '该用户已经是联系人', ['contact' => $contact]); + } + + requireImSuccess( + imAdminPost('/admin/friend/status', [ + 'userId' => $userId, + 'friendUid' => $contact['uid'], + 'status' => 0, + ]), + '添加联系人失败' + ); + requireImSuccess( + imAdminPost('/admin/friend/status', [ + 'userId' => $contact['uid'], + 'friendUid' => $userId, + 'status' => 0, + ]), + '添加联系人失败' + ); + + respond(0, 'success', ['contact' => $contact, 'reason' => $reason]); +} catch (Throwable $error) { + respond(500, $error->getMessage() ?: '添加联系人服务暂不可用'); +} diff --git a/dist-local/api/contacts.php b/dist-local/api/contacts.php new file mode 100644 index 0000000..7dee6d9 --- /dev/null +++ b/dist-local/api/contacts.php @@ -0,0 +1,71 @@ + $userId]), + '联系人加载失败' + ); + $friendIds = array_values(array_unique(array_filter(array_map(static function ($uid): string { + return trim((string)$uid); + }, $friendResult['list'] ?? [])))); + if (!$friendIds) { + respond(0, 'success', ['contacts' => []]); + } + + $usersResult = requireImSuccess( + imAdminPost('/admin/user/batch_get_infos', ['list' => $friendIds]), + '联系人资料加载失败' + ); + try { + ensureWebImAppTables(); + $remarkMap = fetchWebUserRemarkMap(imAppDb(), $userId, $friendIds); + } catch (Throwable) { + $remarkMap = []; + } + + $contactsByUid = []; + foreach (array_values(array_filter($usersResult['userInfos'] ?? [], 'is_array')) as $user) { + $uid = (string)($user['userId'] ?? ''); + if ($uid === '' || isset($contactsByUid[$uid])) { + continue; + } + $alias = ''; + try { + $aliasResult = requireImSuccess( + imAdminPost('/admin/friend/get_alias', ['operator' => $userId, 'targetId' => $uid]), + '联系人备注加载失败' + ); + $alias = trim((string)($aliasResult['alias'] ?? '')); + } catch (Throwable $error) { + $alias = ''; + } + if ($alias === '') { + $alias = $remarkMap[$uid] ?? ''; + } + $contactsByUid[$uid] = [ + 'uid' => $uid, + 'name' => (string)($user['name'] ?? $uid), + 'displayName' => (string)($user['displayName'] ?? $user['name'] ?? $uid), + 'mobile' => (string)($user['mobile'] ?? ''), + 'portrait' => (string)($user['portrait'] ?? ''), + 'address' => (string)($user['address'] ?? ''), + 'alias' => $alias, + ]; + } + + respond(0, 'success', ['contacts' => array_values($contactsByUid)]); +} catch (Throwable $error) { + respond(500, $error->getMessage() ?: '联系人服务暂不可用'); +} diff --git a/dist-local/api/conversation_actions.php b/dist-local/api/conversation_actions.php new file mode 100644 index 0000000..299cc7e --- /dev/null +++ b/dist-local/api/conversation_actions.php @@ -0,0 +1,990 @@ +prepare( + 'select `_uid` from `t_user` where `_uid` = ? or `_mobile` = ? or `_name` = ? limit 1' + ); + $statement->execute([$value, $value, $value]); + $uid = $statement->fetchColumn(); + if (is_string($uid) && $uid !== '') { + $resolved[] = $uid; + } + } + + return array_values(array_unique($resolved)); +} + +function webGroupConversation(array $group, string $role = 'member'): array +{ + $name = (string)($group['name'] ?? $group['_name'] ?? $group['group_id'] ?? $group['_gid'] ?? ''); + $groupId = (string)($group['group_id'] ?? $group['_gid'] ?? ''); + $category = (string)($group['category'] ?? 'group'); + $title = $name !== '' ? $name : $groupId; + $updatedAt = (int)($group['updated_at_ms'] ?? $group['_dt'] ?? webImNowMs()); + + return [ + 'id' => 'group-' . $groupId, + 'type' => $category === 'channel' ? 'channel' : 'group', + 'target' => $groupId, + 'title' => $title, + 'subtitle' => $category === 'channel' ? '公告群' : '群组', + 'unread' => 0, + 'avatarText' => mb_strtoupper(mb_substr($title, 0, 1, 'UTF-8'), 'UTF-8'), + 'updatedAt' => $updatedAt, + 'role' => $role, + 'raw' => [ + 'type' => 'group', + 'target' => $groupId, + 'line' => 0, + 'category' => $category, + 'role' => $role, + ], + ]; +} + +function fetchWebGroup(PDO $db, string $groupKey): ?array +{ + $statement = $db->prepare( + 'select * from `t_web_groups`' + . ' where `deleted` = 0 and (`group_id` = ? or `name` = ?)' + . ' order by (`group_id` = ?) desc limit 1' + ); + $statement->execute([$groupKey, $groupKey, $groupKey]); + $group = $statement->fetch(); + + return is_array($group) ? $group : null; +} + +function fetchWebGroupRole(PDO $db, string $groupId, string $userId): string +{ + $statement = $db->prepare( + 'select `role` from `t_web_group_members`' + . ' where `group_id` = ? and `user_id` = ? and `status` = 0 limit 1' + ); + $statement->execute([$groupId, $userId]); + $role = $statement->fetchColumn(); + + return is_string($role) && $role !== '' ? $role : 'member'; +} + +function fetchWebGroupMember(PDO $db, string $groupId, string $userId): array +{ + $statement = $db->prepare( + 'select * from `t_web_group_members`' + . ' where `group_id` = ? and `user_id` = ? and `status` = 0 limit 1' + ); + $statement->execute([$groupId, $userId]); + $member = $statement->fetch(); + + return is_array($member) ? $member : []; +} + +function requireWebGroupMember(PDO $db, string $groupId, string $userId): array +{ + $member = fetchWebGroupMember($db, $groupId, $userId); + if (!$member) { + respond(403, '你不在该群组中'); + } + + return $member; +} + +function requireWebGroupRole(PDO $db, string $groupId, string $userId, array $roles): array +{ + $member = requireWebGroupMember($db, $groupId, $userId); + if (!in_array((string)$member['role'], $roles, true)) { + respond(403, '没有群组管理权限'); + } + + return $member; +} + +function upsertWebGroupMember(PDO $db, string $groupId, string $userId, string $role): void +{ + $now = webImNowMs(); + $statement = $db->prepare( + 'insert into `t_web_group_members`' + . ' (`group_id`, `user_id`, `role`, `joined_at_ms`, `updated_at_ms`, `status`)' + . ' values (?, ?, ?, ?, ?, 0)' + . ' on duplicate key update' + . ' `role` = if(`role` = "owner", `role`, values(`role`)),' + . ' `updated_at_ms` = values(`updated_at_ms`),' + . ' `status` = 0' + ); + $statement->execute([$groupId, $userId, $role, $now, $now]); +} + +function createNativeGroup(string $operator, string $groupId, string $name, string $category, array $members): array +{ + $nativeMembers = []; + foreach ($members as $memberId) { + $nativeMembers[] = [ + 'member_id' => $memberId, + 'type' => $memberId === $operator ? 2 : 0, + ]; + } + + return requireImSuccess(imAdminPost('/admin/group/create', [ + 'operator' => $operator, + 'group' => [ + 'group_info' => [ + 'target_id' => $groupId, + 'name' => $name, + 'owner' => $operator, + 'type' => 0, + 'mute' => $category === 'channel' ? 1 : 0, + 'join_type' => 0, + 'searchable' => 1, + 'history_message' => 1, + 'extra' => json_encode(['webCategory' => $category], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + ], + 'members' => $nativeMembers, + ], + 'to_lines' => [0], + ]), '原生群组创建失败'); +} + +function addNativeGroupMember(string $operator, string $groupId, string $userId): void +{ + requireImSuccess(imAdminPost('/admin/group/member/add', [ + 'operator' => $operator, + 'group_id' => $groupId, + 'members' => [ + ['member_id' => $userId, 'type' => 0], + ], + 'to_lines' => [0], + ]), '加入原生群组失败'); +} + +function removeNativeGroupMember(string $operator, string $groupId, array $userIds): void +{ + if (!$userIds) { + return; + } + + requireImSuccess(imAdminPost('/admin/group/member/del', [ + 'operator' => $operator, + 'group_id' => $groupId, + 'members' => array_values($userIds), + 'to_lines' => [0], + ]), '移除原生成员失败'); +} + +function dismissNativeGroup(string $operator, string $groupId): void +{ + requireImSuccess(imAdminPost('/admin/group/del', [ + 'operator' => $operator, + 'group_id' => $groupId, + 'to_lines' => [0], + ]), '解散原生群组失败'); +} + +function transferNativeGroup(string $operator, string $groupId, string $newOwner): void +{ + requireImSuccess(imAdminPost('/admin/group/transfer', [ + 'operator' => $operator, + 'group_id' => $groupId, + 'new_owner' => $newOwner, + 'to_lines' => [0], + ]), '转让原生群组失败'); +} + +function sendWebGroupWelcomeMessage(PDO $appDb, string $groupId, string $senderId, string $welcomeText): void +{ + $text = trim($welcomeText); + if ($text === '') { + return; + } + + $now = webImNowMs(); + $messageUid = webImMessageUid(); + $statement = $appDb->prepare( + 'insert into `t_web_group_messages`' + . ' (`message_uid`, `group_id`, `sender_uid`, `text`, `created_at_ms`, `edited_at_ms`, `recalled`)' + . ' values (?, ?, ?, ?, ?, 0, 0)' + ); + $statement->execute([$messageUid, $groupId, $senderId, $text, $now]); + $update = $appDb->prepare('update `t_web_groups` set `updated_at_ms` = ? where `group_id` = ?'); + $update->execute([$now, $groupId]); + + try { + requireImSuccess(imAdminPost('/admin/message/send', [ + 'sender' => $senderId, + 'conv' => [ + 'type' => 1, + 'target' => $groupId, + 'line' => 0, + ], + 'payload' => [ + 'type' => 1, + 'searchableContent' => $text, + 'pushContent' => $text, + 'content' => $text, + 'persistFlag' => 3, + ], + 'userMessage' => true, + ]), '欢迎词发送失败'); + } catch (Throwable) { + } +} + +function createWebGroupAction(PDO $imDb, PDO $appDb, string $userId, array $payload, string $category): array +{ + $name = trim((string)($payload['name'] ?? '')); + $announcement = trim((string)($payload['announcement'] ?? '')); + $welcomeText = trim((string)($payload['welcomeText'] ?? '')); + if ($name === '') { + respond(400, $category === 'channel' ? '请输入公告群名称' : '请输入群组名称'); + } + + $memberIds = resolveWebUserIds($imDb, webActionList($payload['memberIds'] ?? [])); + $memberIds[] = $userId; + $memberIds = array_values(array_unique(array_filter($memberIds))); + if ($category === 'group' && count($memberIds) < 2) { + respond(400, '请选择至少一位联系人'); + } + + $groupId = 'webg_' . bin2hex(random_bytes(8)); + $now = webImNowMs(); + $nativeStatus = 'pending'; + $nativeGroupId = ''; + $nativeError = ''; + + try { + $nativeResult = createNativeGroup($userId, $groupId, $name, $category, $memberIds); + $nativeGroupId = (string)($nativeResult['group_id'] ?? $nativeResult['groupId'] ?? $groupId); + $nativeStatus = 'ok'; + } catch (Throwable $error) { + $nativeStatus = 'fallback'; + $nativeError = mb_substr($error->getMessage() ?: '原生群组创建失败', 0, 255, 'UTF-8'); + } + + $statement = $appDb->prepare( + 'insert into `t_web_groups`' + . ' (`group_id`, `category`, `name`, `portrait`, `owner_uid`, `join_mode`, `announcement`,' + . ' `welcome_text`, `native_group_id`, `native_status`, `native_error`, `created_by`, `created_at_ms`, `updated_at_ms`, `deleted`)' + . ' values (?, ?, ?, "", ?, "open", ?, ?, ?, ?, ?, ?, ?, ?, 0)' + ); + $statement->execute([ + $groupId, + $category, + $name, + $userId, + $announcement, + $welcomeText, + $nativeGroupId, + $nativeStatus, + $nativeError, + $userId, + $now, + $now, + ]); + + foreach ($memberIds as $memberId) { + upsertWebGroupMember($appDb, $groupId, $memberId, $memberId === $userId ? 'owner' : 'member'); + } + sendWebGroupWelcomeMessage($appDb, $groupId, $userId, $welcomeText); + + $group = fetchWebGroup($appDb, $groupId); + return [ + 'group' => $group, + 'conversation' => webGroupConversation($group ?: [ + 'group_id' => $groupId, + 'category' => $category, + 'name' => $name, + 'updated_at_ms' => $now, + ], 'owner'), + 'nativeStatus' => $nativeStatus, + 'nativeError' => $nativeError, + ]; +} + +function fetchWebUserMap(PDO $imDb, array $userIds, string $ownerId = ''): array +{ + $userIds = webActionList($userIds); + if (!$userIds) { + return []; + } + + $statement = $imDb->prepare( + 'select `_uid`, `_name`, `_display_name`, `_mobile`, `_portrait` from `t_user`' + . ' where `_uid` in (' . appPlaceholders($userIds) . ')' + ); + $statement->execute($userIds); + + $users = []; + foreach ($statement->fetchAll() as $row) { + $uid = (string)$row['_uid']; + $displayName = trim((string)($row['_display_name'] ?? '')) + ?: trim((string)($row['_name'] ?? '')) + ?: trim((string)($row['_mobile'] ?? '')) + ?: $uid; + $users[$uid] = [ + 'uid' => $uid, + 'name' => (string)($row['_name'] ?? ''), + 'displayName' => $displayName, + 'mobile' => (string)($row['_mobile'] ?? ''), + 'portrait' => (string)($row['_portrait'] ?? ''), + 'contactAlias' => '', + 'friendAlias' => '', + 'remarkAlias' => '', + 'isContact' => false, + ]; + } + + if ($ownerId !== '') { + $aliasStatement = $imDb->prepare( + 'select `_friend_uid`, `_alias` from `t_friend` where `_uid` = ?' + . ' and `_friend_uid` in (' . appPlaceholders($userIds) . ')' + ); + $aliasStatement->execute(array_merge([$ownerId], $userIds)); + foreach ($aliasStatement->fetchAll() as $row) { + $friendUid = (string)$row['_friend_uid']; + if (!isset($users[$friendUid])) { + continue; + } + $users[$friendUid]['isContact'] = true; + $users[$friendUid]['friendAlias'] = trim((string)($row['_alias'] ?? '')); + } + + $remarkMap = fetchWebUserRemarkMap(imAppDb(), $ownerId, $userIds); + foreach ($remarkMap as $targetUid => $alias) { + if (!isset($users[$targetUid])) { + continue; + } + $users[$targetUid]['remarkAlias'] = $alias; + } + + foreach ($users as $uid => $user) { + $friendAlias = trim((string)($user['friendAlias'] ?? '')); + $remarkAlias = trim((string)($user['remarkAlias'] ?? '')); + $users[$uid]['contactAlias'] = $friendAlias !== '' ? $friendAlias : $remarkAlias; + } + } + + return $users; +} + +function currentMuteSetting(PDO $appDb, string $groupId): array +{ + $statement = $appDb->prepare('select * from `t_web_group_mute_rules` where `group_id` = ? limit 1'); + $statement->execute([$groupId]); + $row = $statement->fetch(); + if (!is_array($row)) { + return [ + 'mode' => 'off', + 'scheduleType' => 'once', + 'startsAtMs' => 0, + 'endsAtMs' => 0, + 'dailyStart' => '', + 'dailyEnd' => '', + 'enabled' => false, + ]; + } + + return [ + 'mode' => (string)$row['mode'], + 'scheduleType' => (string)$row['schedule_type'], + 'startsAtMs' => (int)$row['starts_at_ms'], + 'endsAtMs' => (int)$row['ends_at_ms'], + 'dailyStart' => (string)$row['daily_start'], + 'dailyEnd' => (string)$row['daily_end'], + 'enabled' => (bool)$row['enabled'], + ]; +} + +function groupDetailAction(PDO $imDb, PDO $appDb, string $userId, string $groupId): array +{ + $group = fetchWebGroup($appDb, $groupId); + if (!$group) { + respond(404, '没有找到该群组'); + } + $member = fetchWebGroupMember($appDb, $groupId, $userId); + if (!$member) { + respond(403, '你不在该群组中'); + } + + $ownerId = (string)$group['owner_uid']; + $users = fetchWebUserMap($imDb, [$ownerId, $userId], $userId); + $owner = $users[$ownerId] ?? ['uid' => $ownerId, 'displayName' => $ownerId, 'contactAlias' => '']; + $me = $users[$userId] ?? ['uid' => $userId, 'displayName' => $userId]; + $myAlias = (string)($member['alias'] ?? ''); + + return [ + 'group' => [ + 'groupId' => (string)$group['group_id'], + 'name' => (string)$group['name'], + 'category' => (string)$group['category'], + 'announcement' => (string)($group['announcement'] ?? ''), + 'welcomeText' => (string)($group['welcome_text'] ?? ''), + 'ownerUid' => $ownerId, + 'ownerName' => (string)($owner['contactAlias'] ?? '') !== '' ? (string)$owner['contactAlias'] : (string)$owner['displayName'], + 'myRole' => (string)$member['role'], + 'myAlias' => $myAlias, + 'myDisplayName' => $myAlias !== '' ? $myAlias : (string)$me['displayName'], + 'muteSetting' => currentMuteSetting($appDb, (string)$group['group_id']), + ], + ]; +} + +function groupMembersAction(PDO $imDb, PDO $appDb, string $userId, string $groupId): array +{ + if (!fetchWebGroupMember($appDb, $groupId, $userId)) { + respond(403, '你不在该群组中'); + } + + $statement = $appDb->prepare( + 'select `user_id`, `role`, `alias`, `joined_at_ms` from `t_web_group_members`' + . ' where `group_id` = ? and `status` = 0 order by (`role` = "owner") desc, `joined_at_ms` asc' + ); + $statement->execute([$groupId]); + $members = $statement->fetchAll(); + $users = fetchWebUserMap($imDb, array_column($members, 'user_id'), $userId); + + return [ + 'members' => array_map(static function (array $member) use ($users): array { + $uid = (string)$member['user_id']; + $user = $users[$uid] ?? [ + 'uid' => $uid, + 'displayName' => $uid, + 'mobile' => '', + 'portrait' => '', + 'contactAlias' => '', + 'friendAlias' => '', + 'remarkAlias' => '', + 'isContact' => false, + ]; + $alias = trim((string)($member['alias'] ?? '')); + $contactAlias = trim((string)($user['contactAlias'] ?? '')); + return [ + 'uid' => $uid, + 'role' => (string)$member['role'], + 'alias' => $alias, + 'contactAlias' => $contactAlias, + 'friendAlias' => trim((string)($user['friendAlias'] ?? '')), + 'remarkAlias' => trim((string)($user['remarkAlias'] ?? '')), + 'isContact' => (bool)($user['isContact'] ?? false), + 'displayName' => $contactAlias !== '' ? $contactAlias : ($alias !== '' ? $alias : (string)$user['displayName']), + 'rawDisplayName' => (string)$user['displayName'], + 'groupDisplayName' => $alias, + 'mobile' => (string)($user['mobile'] ?? ''), + 'portrait' => (string)($user['portrait'] ?? ''), + ]; + }, $members), + ]; +} + +function updateGroupAliasAction(PDO $appDb, string $userId, string $groupId, string $alias): array +{ + if (mb_strlen($alias, 'UTF-8') > 64) { + respond(400, '本群昵称过长'); + } + if (!fetchWebGroupMember($appDb, $groupId, $userId)) { + respond(403, '你不在该群组中'); + } + + $now = webImNowMs(); + $statement = $appDb->prepare( + 'update `t_web_group_members` set `alias` = ?, `updated_at_ms` = ?' + . ' where `group_id` = ? and `user_id` = ? and `status` = 0' + ); + $statement->execute([$alias, $now, $groupId, $userId]); + + return ['alias' => $alias]; +} + +function leaveGroupAction(PDO $appDb, string $userId, string $groupId): array +{ + $group = fetchWebGroup($appDb, $groupId); + if (!$group) { + respond(404, '没有找到该群组'); + } + $member = fetchWebGroupMember($appDb, $groupId, $userId); + if (!$member) { + respond(403, '你不在该群组中'); + } + + $now = webImNowMs(); + $statement = $appDb->prepare( + 'update `t_web_group_members` set `status` = 1, `updated_at_ms` = ?' + . ' where `group_id` = ? and `user_id` = ?' + ); + $statement->execute([$now, $groupId, $userId]); + $groupUpdate = $appDb->prepare('update `t_web_groups` set `updated_at_ms` = ? where `group_id` = ?'); + $groupUpdate->execute([$now, $groupId]); + + try { + requireImSuccess(imAdminPost('/admin/group/member/quit', [ + 'operator' => $userId, + 'group_id' => $groupId, + 'to_lines' => [0], + ]), '退出原生群组失败'); + } catch (Throwable) { + } + + return ['groupId' => $groupId]; +} + +function dismissGroupAction(PDO $appDb, string $userId, string $groupId): array +{ + $group = fetchWebGroup($appDb, $groupId); + if (!$group) { + respond(404, '没有找到该群组'); + } + requireWebGroupRole($appDb, $groupId, $userId, ['owner']); + + $now = webImNowMs(); + $update = $appDb->prepare('update `t_web_groups` set `deleted` = 1, `updated_at_ms` = ? where `group_id` = ?'); + $update->execute([$now, $groupId]); + $memberUpdate = $appDb->prepare('update `t_web_group_members` set `status` = 1, `updated_at_ms` = ? where `group_id` = ?'); + $memberUpdate->execute([$now, $groupId]); + + try { + dismissNativeGroup($userId, $groupId); + } catch (Throwable) { + } + + return ['groupId' => $groupId]; +} + +function transferGroupAction(PDO $appDb, string $userId, string $groupId, string $targetUserId): array +{ + if ($targetUserId === '' || $targetUserId === $userId) { + respond(400, '请选择新的群主'); + } + $group = fetchWebGroup($appDb, $groupId); + if (!$group) { + respond(404, '没有找到该群组'); + } + requireWebGroupRole($appDb, $groupId, $userId, ['owner']); + $target = requireWebGroupMember($appDb, $groupId, $targetUserId); + if ((string)$target['role'] === 'owner') { + respond(400, '该成员已经是群主'); + } + + $now = webImNowMs(); + $appDb->beginTransaction(); + try { + $groupUpdate = $appDb->prepare('update `t_web_groups` set `owner_uid` = ?, `updated_at_ms` = ? where `group_id` = ?'); + $groupUpdate->execute([$targetUserId, $now, $groupId]); + $oldOwnerUpdate = $appDb->prepare( + 'update `t_web_group_members` set `role` = "member", `updated_at_ms` = ?' + . ' where `group_id` = ? and `user_id` = ?' + ); + $oldOwnerUpdate->execute([$now, $groupId, $userId]); + $newOwnerUpdate = $appDb->prepare( + 'update `t_web_group_members` set `role` = "owner", `updated_at_ms` = ?' + . ' where `group_id` = ? and `user_id` = ?' + ); + $newOwnerUpdate->execute([$now, $groupId, $targetUserId]); + $appDb->commit(); + } catch (Throwable $error) { + $appDb->rollBack(); + throw $error; + } + + try { + transferNativeGroup($userId, $groupId, $targetUserId); + } catch (Throwable) { + } + + return ['groupId' => $groupId, 'newOwner' => $targetUserId]; +} + +function setGroupAdminsAction(PDO $appDb, string $userId, string $groupId, array $adminIds): array +{ + requireWebGroupRole($appDb, $groupId, $userId, ['owner']); + $adminIds = array_values(array_unique(array_filter(array_map('strval', $adminIds), static fn(string $uid): bool => $uid !== $userId))); + $now = webImNowMs(); + + $reset = $appDb->prepare( + 'update `t_web_group_members` set `role` = "member", `updated_at_ms` = ?' + . ' where `group_id` = ? and `role` = "admin" and `status` = 0' + ); + $reset->execute([$now, $groupId]); + + if ($adminIds) { + $update = $appDb->prepare( + 'update `t_web_group_members` set `role` = "admin", `updated_at_ms` = ?' + . ' where `group_id` = ? and `user_id` in (' . appPlaceholders($adminIds) . ')' + . ' and `role` <> "owner" and `status` = 0' + ); + $update->execute(array_merge([$now, $groupId], $adminIds)); + } + + return ['adminIds' => $adminIds]; +} + +function updateGroupProfileAction(PDO $appDb, string $userId, string $groupId, array $payload): array +{ + requireWebGroupRole($appDb, $groupId, $userId, ['owner', 'admin']); + $name = trim((string)($payload['name'] ?? '')); + $announcement = trim((string)($payload['announcement'] ?? '')); + $welcomeText = trim((string)($payload['welcomeText'] ?? '')); + if ($name === '') { + respond(400, '请输入群名称'); + } + if (mb_strlen($name, 'UTF-8') > 128) { + respond(400, '群名称过长'); + } + + $now = webImNowMs(); + $statement = $appDb->prepare( + 'update `t_web_groups` set `name` = ?, `announcement` = ?, `welcome_text` = ?, `updated_at_ms` = ?' + . ' where `group_id` = ? and `deleted` = 0' + ); + $statement->execute([$name, $announcement, $welcomeText, $now, $groupId]); + + return [ + 'groupId' => $groupId, + 'name' => $name, + 'announcement' => $announcement, + 'welcomeText' => $welcomeText, + 'updatedAt' => $now, + ]; +} + +function kickGroupMembersAction(PDO $appDb, string $userId, string $groupId, array $targetIds): array +{ + $operator = requireWebGroupRole($appDb, $groupId, $userId, ['owner', 'admin']); + $operatorRole = (string)$operator['role']; + $targetIds = array_values(array_unique(array_filter(array_map('strval', $targetIds), static fn(string $uid): bool => $uid !== ''))); + $targetIds = array_values(array_filter($targetIds, static fn(string $uid): bool => $uid !== $userId)); + if (!$targetIds) { + respond(400, '请选择要移除的群员'); + } + + $statement = $appDb->prepare( + 'select `user_id`, `role` from `t_web_group_members`' + . ' where `group_id` = ? and `user_id` in (' . appPlaceholders($targetIds) . ') and `status` = 0' + ); + $statement->execute(array_merge([$groupId], $targetIds)); + $targets = $statement->fetchAll(); + if (!$targets) { + respond(400, '没有可移除的群员'); + } + + $allowedIds = []; + foreach ($targets as $target) { + $role = (string)$target['role']; + if ($role === 'owner') { + continue; + } + if ($operatorRole === 'admin' && $role !== 'member') { + continue; + } + $allowedIds[] = (string)$target['user_id']; + } + if (!$allowedIds) { + respond(403, '没有可移除的群员'); + } + + $now = webImNowMs(); + $update = $appDb->prepare( + 'update `t_web_group_members` set `status` = 2, `updated_at_ms` = ?' + . ' where `group_id` = ? and `user_id` in (' . appPlaceholders($allowedIds) . ')' + ); + $update->execute(array_merge([$now, $groupId], $allowedIds)); + + try { + removeNativeGroupMember($userId, $groupId, $allowedIds); + } catch (Throwable) { + } + + return ['removedIds' => $allowedIds]; +} + +function updateMuteSettingAction(PDO $appDb, string $userId, string $groupId, array $payload): array +{ + requireWebGroupRole($appDb, $groupId, $userId, ['owner', 'admin']); + $mode = trim((string)($payload['mode'] ?? 'off')); + $allowedModes = ['off', 'all_include_admins', 'all_except_admins']; + if (!in_array($mode, $allowedModes, true)) { + respond(400, '禁言模式不正确'); + } + $scheduleType = trim((string)($payload['scheduleType'] ?? 'once')); + if (!in_array($scheduleType, ['manual', 'once', 'daily'], true)) { + respond(400, '禁言时段类型不正确'); + } + + $startsAtMs = max(0, (int)($payload['startsAtMs'] ?? 0)); + $endsAtMs = max(0, (int)($payload['endsAtMs'] ?? 0)); + $dailyStart = trim((string)($payload['dailyStart'] ?? '')); + $dailyEnd = trim((string)($payload['dailyEnd'] ?? '')); + $enabled = $mode !== 'off'; + if ($enabled && $scheduleType === 'once' && ($startsAtMs <= 0 || $endsAtMs <= $startsAtMs)) { + respond(400, '请设置有效的单次禁言时段'); + } + if ($enabled && $scheduleType === 'daily' && (!preg_match('/^\d{2}:\d{2}$/', $dailyStart) || !preg_match('/^\d{2}:\d{2}$/', $dailyEnd))) { + respond(400, '请设置有效的每日禁言时段'); + } + + $now = webImNowMs(); + $statement = $appDb->prepare( + 'insert into `t_web_group_mute_rules`' + . ' (`group_id`, `mode`, `schedule_type`, `starts_at_ms`, `ends_at_ms`, `daily_start`, `daily_end`, `enabled`, `created_by`, `updated_at_ms`)' + . ' values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' + . ' on duplicate key update' + . ' `mode` = values(`mode`),' + . ' `schedule_type` = values(`schedule_type`),' + . ' `starts_at_ms` = values(`starts_at_ms`),' + . ' `ends_at_ms` = values(`ends_at_ms`),' + . ' `daily_start` = values(`daily_start`),' + . ' `daily_end` = values(`daily_end`),' + . ' `enabled` = values(`enabled`),' + . ' `created_by` = values(`created_by`),' + . ' `updated_at_ms` = values(`updated_at_ms`)' + ); + $statement->execute([$groupId, $mode, $scheduleType, $startsAtMs, $endsAtMs, $dailyStart, $dailyEnd, $enabled ? 1 : 0, $userId, $now]); + + return ['muteSetting' => currentMuteSetting($appDb, $groupId)]; +} + +function joinWebGroupAction(PDO $appDb, string $userId, string $groupKey): array +{ + if ($groupKey === '') { + respond(400, '请输入群组ID或群名称'); + } + + $group = fetchWebGroup($appDb, $groupKey); + if (!$group) { + try { + $native = requireImSuccess(imAdminPost('/admin/group/get_info', ['groupId' => $groupKey]), '群组不存在'); + $now = webImNowMs(); + $statement = $appDb->prepare( + 'insert into `t_web_groups`' + . ' (`group_id`, `category`, `name`, `portrait`, `owner_uid`, `join_mode`, `announcement`,' + . ' `native_group_id`, `native_status`, `created_by`, `created_at_ms`, `updated_at_ms`, `deleted`)' + . ' values (?, "group", ?, ?, ?, "open", "", ?, "ok", ?, ?, ?, 0)' + . ' on duplicate key update `updated_at_ms` = values(`updated_at_ms`)' + ); + $statement->execute([ + $groupKey, + (string)($native['name'] ?? $groupKey), + (string)($native['portrait'] ?? ''), + (string)($native['owner'] ?? ''), + $groupKey, + $userId, + $now, + $now, + ]); + $group = fetchWebGroup($appDb, $groupKey); + } catch (Throwable) { + respond(404, '没有找到该群组'); + } + } + + $groupId = (string)$group['group_id']; + upsertWebGroupMember($appDb, $groupId, $userId, 'member'); + try { + addNativeGroupMember((string)($group['owner_uid'] ?: $userId), $groupId, $userId); + } catch (Throwable $error) { + // Web membership is authoritative for this web build; native group sync is best-effort. + } + + $role = fetchWebGroupRole($appDb, $groupId, $userId); + return [ + 'group' => fetchWebGroup($appDb, $groupId), + 'conversation' => webGroupConversation($group, $role), + ]; +} + +function sendBroadcastAction(PDO $imDb, PDO $appDb, string $userId, array $payload): array +{ + $text = trim((string)($payload['text'] ?? '')); + if ($text === '') { + respond(400, '请输入群发内容'); + } + + $targetIds = resolveWebUserIds($imDb, webActionList($payload['targetIds'] ?? [])); + $targetIds = array_values(array_filter(array_unique($targetIds), static fn(string $uid): bool => $uid !== $userId)); + if (!$targetIds) { + respond(400, '请选择群发对象'); + } + + $results = []; + foreach ($targetIds as $targetId) { + try { + $result = requireImSuccess(imAdminPost('/admin/message/send', [ + 'sender' => $userId, + 'conv' => [ + 'type' => 0, + 'target' => $targetId, + 'line' => 0, + ], + 'payload' => [ + 'type' => 1, + 'searchableContent' => $text, + 'pushContent' => $text, + 'content' => $text, + 'persistFlag' => 3, + ], + 'userMessage' => true, + ]), '发送失败'); + $results[] = [ + 'targetId' => $targetId, + 'ok' => true, + 'messageUid' => isset($result['messageUid']) ? (string)$result['messageUid'] : '', + ]; + } catch (Throwable $error) { + $results[] = [ + 'targetId' => $targetId, + 'ok' => false, + 'error' => $error->getMessage() ?: '发送失败', + ]; + } + } + + $successCount = count(array_filter($results, static fn(array $item): bool => !empty($item['ok']))); + $failedCount = count($results) - $successCount; + $now = webImNowMs(); + $statement = $appDb->prepare( + 'insert into `t_web_broadcast_logs`' + . ' (`sender_uid`, `target_count`, `success_count`, `failed_count`, `text`, `target_json`, `result_json`, `created_at_ms`)' + . ' values (?, ?, ?, ?, ?, ?, ?, ?)' + ); + $statement->execute([ + $userId, + count($targetIds), + $successCount, + $failedCount, + $text, + json_encode($targetIds, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + json_encode($results, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + $now, + ]); + + return [ + 'targetCount' => count($targetIds), + 'successCount' => $successCount, + 'failedCount' => $failedCount, + 'results' => $results, + ]; +} + +$payload = readJsonPayload(); +$action = trim((string)($payload['action'] ?? '')); +$userId = trim((string)($payload['userId'] ?? '')); + +if ($userId === '') { + respond(400, '缺少当前用户ID'); +} + +try { + ensureWebImAppTables(); + $imDb = imDb(); + $appDb = imAppDb(); + + if ($action === 'create_group') { + respond(0, 'success', createWebGroupAction($imDb, $appDb, $userId, $payload, 'group')); + } + if ($action === 'create_channel') { + respond(0, 'success', createWebGroupAction($imDb, $appDb, $userId, $payload, 'channel')); + } + if ($action === 'join_group') { + respond(0, 'success', joinWebGroupAction($appDb, $userId, trim((string)($payload['groupKey'] ?? '')))); + } + if ($action === 'group_detail') { + respond(0, 'success', groupDetailAction($imDb, $appDb, $userId, trim((string)($payload['groupId'] ?? '')))); + } + if ($action === 'group_members') { + respond(0, 'success', groupMembersAction($imDb, $appDb, $userId, trim((string)($payload['groupId'] ?? '')))); + } + if ($action === 'update_group_alias') { + respond(0, 'success', updateGroupAliasAction( + $appDb, + $userId, + trim((string)($payload['groupId'] ?? '')), + trim((string)($payload['alias'] ?? '')) + )); + } + if ($action === 'leave_group') { + respond(0, 'success', leaveGroupAction($appDb, $userId, trim((string)($payload['groupId'] ?? '')))); + } + if ($action === 'dismiss_group') { + respond(0, 'success', dismissGroupAction($appDb, $userId, trim((string)($payload['groupId'] ?? '')))); + } + if ($action === 'transfer_group') { + respond(0, 'success', transferGroupAction( + $appDb, + $userId, + trim((string)($payload['groupId'] ?? '')), + trim((string)($payload['targetUserId'] ?? '')) + )); + } + if ($action === 'set_group_admins') { + respond(0, 'success', setGroupAdminsAction( + $appDb, + $userId, + trim((string)($payload['groupId'] ?? '')), + is_array($payload['adminIds'] ?? null) ? $payload['adminIds'] : [] + )); + } + if ($action === 'update_group_profile') { + respond(0, 'success', updateGroupProfileAction( + $appDb, + $userId, + trim((string)($payload['groupId'] ?? '')), + $payload + )); + } + if ($action === 'kick_group_members') { + respond(0, 'success', kickGroupMembersAction( + $appDb, + $userId, + trim((string)($payload['groupId'] ?? '')), + is_array($payload['targetIds'] ?? null) ? $payload['targetIds'] : [] + )); + } + if ($action === 'update_mute_setting') { + respond(0, 'success', updateMuteSettingAction( + $appDb, + $userId, + trim((string)($payload['groupId'] ?? '')), + $payload + )); + } + if ($action === 'broadcast') { + $result = sendBroadcastAction($imDb, $appDb, $userId, $payload); + if ((int)$result['successCount'] <= 0) { + respond(500, '群发失败', $result); + } + respond(0, 'success', $result); + } + + respond(400, '未知操作'); +} catch (Throwable $error) { + respond(500, $error->getMessage() ?: '会话操作失败'); +} diff --git a/dist-local/api/im_web_bootstrap.php b/dist-local/api/im_web_bootstrap.php new file mode 100644 index 0000000..7adbbcd --- /dev/null +++ b/dist-local/api/im_web_bootstrap.php @@ -0,0 +1,348 @@ + $code, + 'message' => $message, + 'result' => $result, + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; +} + +function readJsonPayload(): array +{ + $payload = json_decode(file_get_contents('php://input') ?: '[]', true); + return is_array($payload) ? $payload : []; +} + +function imAdminPost(string $path, array $payload): array +{ + $nonce = (string)random_int(1, 1000000); + $timestamp = (string)((int)floor(microtime(true) * 1000)); + $sign = sha1($nonce . '|' . IM_ADMIN_SECRET . '|' . $timestamp); + $body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + + $curl = curl_init(IM_ADMIN_URL . $path); + if ($curl === false) { + throw new RuntimeException('IM admin client init failed'); + } + + curl_setopt_array($curl, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $body, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_0, + CURLOPT_CONNECTTIMEOUT_MS => 1000, + CURLOPT_TIMEOUT_MS => 4000, + CURLOPT_FRESH_CONNECT => true, + CURLOPT_FORBID_REUSE => true, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json; charset=utf-8', + 'Connection: close', + 'nonce: ' . $nonce, + 'timestamp: ' . $timestamp, + 'sign: ' . $sign, + ], + ]); + + $responseBody = curl_exec($curl); + $curlError = curl_error($curl); + curl_close($curl); + + if ($responseBody === false || $responseBody === '') { + throw new RuntimeException($curlError ?: 'IM admin request failed'); + } + + $data = json_decode($responseBody, true); + if (!is_array($data)) { + throw new RuntimeException('Invalid IM admin response'); + } + + return $data; +} + +function requireImSuccess(array $response, string $fallbackMessage): array +{ + if (($response['code'] ?? -1) !== 0) { + throw new RuntimeException((string)($response['msg'] ?? $response['message'] ?? $fallbackMessage)); + } + + return is_array($response['result'] ?? null) ? $response['result'] : []; +} + +function imDb(): PDO +{ + static $pdo = null; + if ($pdo instanceof PDO) { + return $pdo; + } + + $pdo = new PDO(IM_DB_DSN, IM_DB_USER, IM_DB_PASSWORD, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]); + + return $pdo; +} + +function imAppDb(): PDO +{ + static $pdo = null; + if ($pdo instanceof PDO) { + return $pdo; + } + + $pdo = new PDO(IM_APP_DB_DSN, IM_APP_DB_USER, IM_APP_DB_PASSWORD, [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]); + + return $pdo; +} + +function ensureWebImAppTables(): void +{ + static $ensured = false; + if ($ensured) { + return; + } + $ensured = true; + + $db = imAppDb(); + $db->exec( + "create table if not exists `t_web_groups` ( + `id` bigint unsigned not null auto_increment, + `group_id` varchar(64) not null, + `category` varchar(20) not null default 'group', + `name` varchar(128) not null, + `portrait` varchar(1024) not null default '', + `owner_uid` varchar(64) not null, + `join_mode` varchar(20) not null default 'open', + `announcement` text null, + `native_group_id` varchar(64) not null default '', + `native_status` varchar(32) not null default 'pending', + `native_error` varchar(255) not null default '', + `created_by` varchar(64) not null, + `created_at_ms` bigint not null, + `updated_at_ms` bigint not null, + `deleted` tinyint not null default 0, + primary key (`id`), + unique key `uk_t_web_groups_group_id` (`group_id`), + key `idx_t_web_groups_owner` (`owner_uid`), + key `idx_t_web_groups_category` (`category`) + ) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_unicode_ci" + ); + try { + $db->exec("alter table `t_web_groups` add column `welcome_text` text null after `announcement`"); + } catch (Throwable) { + } + $db->exec( + "create table if not exists `t_web_group_members` ( + `id` bigint unsigned not null auto_increment, + `group_id` varchar(64) not null, + `user_id` varchar(64) not null, + `role` varchar(20) not null default 'member', + `alias` varchar(128) not null default '', + `joined_at_ms` bigint not null, + `updated_at_ms` bigint not null, + `status` tinyint not null default 0, + primary key (`id`), + unique key `uk_t_web_group_members_gid_uid` (`group_id`, `user_id`), + key `idx_t_web_group_members_user` (`user_id`) + ) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_unicode_ci" + ); + $db->exec( + "create table if not exists `t_web_group_messages` ( + `id` bigint unsigned not null auto_increment, + `message_uid` varchar(64) not null, + `group_id` varchar(64) not null, + `sender_uid` varchar(64) not null, + `text` text not null, + `created_at_ms` bigint not null, + `edited_at_ms` bigint not null default 0, + `recalled` tinyint not null default 0, + primary key (`id`), + unique key `uk_t_web_group_messages_uid` (`message_uid`), + key `idx_t_web_group_messages_group_time` (`group_id`, `created_at_ms`), + key `idx_t_web_group_messages_sender` (`sender_uid`) + ) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_unicode_ci" + ); + $db->exec( + "create table if not exists `t_web_message_attachments` ( + `id` bigint unsigned not null auto_increment, + `message_uid` varchar(64) not null, + `conversation_type` varchar(16) not null default 'single', + `target_id` varchar(128) not null default '', + `sender_uid` varchar(64) not null default '', + `attachment_type` varchar(16) not null, + `url` varchar(1024) not null, + `name` varchar(255) not null default '', + `mime` varchar(128) not null default '', + `size_bytes` bigint not null default 0, + `created_at_ms` bigint not null, + primary key (`id`), + unique key `uk_t_web_message_attachments_uid` (`message_uid`), + key `idx_t_web_message_attachments_target` (`conversation_type`, `target_id`, `created_at_ms`) + ) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_unicode_ci" + ); + $db->exec( + "create table if not exists `t_web_group_reads` ( + `id` bigint unsigned not null auto_increment, + `group_id` varchar(64) not null, + `user_id` varchar(64) not null, + `read_at_ms` bigint not null default 0, + `updated_at_ms` bigint not null, + primary key (`id`), + unique key `uk_t_web_group_reads_gid_uid` (`group_id`, `user_id`) + ) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_unicode_ci" + ); + $db->exec( + "create table if not exists `t_web_group_mute_rules` ( + `id` bigint unsigned not null auto_increment, + `group_id` varchar(64) not null, + `mode` varchar(32) not null default 'off', + `schedule_type` varchar(16) not null default 'once', + `starts_at_ms` bigint not null default 0, + `ends_at_ms` bigint not null default 0, + `daily_start` varchar(8) not null default '', + `daily_end` varchar(8) not null default '', + `enabled` tinyint not null default 0, + `created_by` varchar(64) not null default '', + `updated_at_ms` bigint not null, + primary key (`id`), + unique key `uk_t_web_group_mute_rules_group` (`group_id`) + ) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_unicode_ci" + ); + $db->exec( + "create table if not exists `t_web_broadcast_logs` ( + `id` bigint unsigned not null auto_increment, + `sender_uid` varchar(64) not null, + `target_count` int not null default 0, + `success_count` int not null default 0, + `failed_count` int not null default 0, + `text` text not null, + `target_json` text null, + `result_json` text null, + `created_at_ms` bigint not null, + primary key (`id`), + key `idx_t_web_broadcast_logs_sender_time` (`sender_uid`, `created_at_ms`) + ) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_unicode_ci" + ); + $db->exec( + "create table if not exists `t_web_user_remarks` ( + `id` bigint unsigned not null auto_increment, + `owner_uid` varchar(64) not null, + `target_uid` varchar(64) not null, + `alias` varchar(128) not null default '', + `created_at_ms` bigint not null, + `updated_at_ms` bigint not null, + primary key (`id`), + unique key `uk_t_web_user_remarks_owner_target` (`owner_uid`, `target_uid`), + key `idx_t_web_user_remarks_target` (`target_uid`) + ) engine=InnoDB default charset=utf8mb4 collate=utf8mb4_unicode_ci" + ); +} + +function webImPlaceholders(array $values): string +{ + return implode(',', array_fill(0, count($values), '?')); +} + +function fetchWebUserRemarkMap(PDO $db, string $ownerId, array $targetIds): array +{ + $targetIds = array_values(array_unique(array_filter(array_map(static function ($targetId): string { + return trim((string)$targetId); + }, $targetIds)))); + if ($ownerId === '' || !$targetIds) { + return []; + } + + $statement = $db->prepare( + 'select `target_uid`, `alias` from `t_web_user_remarks`' + . ' where `owner_uid` = ? and `target_uid` in (' . webImPlaceholders($targetIds) . ')' + ); + $statement->execute(array_merge([$ownerId], $targetIds)); + + $remarks = []; + foreach ($statement->fetchAll() as $row) { + $remarks[(string)$row['target_uid']] = trim((string)($row['alias'] ?? '')); + } + + return $remarks; +} + +function saveWebUserRemark(PDO $db, string $ownerId, string $targetId, string $alias): void +{ + if ($ownerId === '' || $targetId === '') { + return; + } + + if ($alias === '') { + $delete = $db->prepare('delete from `t_web_user_remarks` where `owner_uid` = ? and `target_uid` = ?'); + $delete->execute([$ownerId, $targetId]); + return; + } + + $now = webImNowMs(); + $statement = $db->prepare( + 'insert into `t_web_user_remarks` (`owner_uid`, `target_uid`, `alias`, `created_at_ms`, `updated_at_ms`)' + . ' values (?, ?, ?, ?, ?)' + . ' on duplicate key update `alias` = values(`alias`), `updated_at_ms` = values(`updated_at_ms`)' + ); + $statement->execute([$ownerId, $targetId, $alias, $now, $now]); +} + +function webImNowMs(): int +{ + return (int)floor(microtime(true) * 1000); +} + +function webImMessageUid(): string +{ + $timestampPart = (webImNowMs() - 1514736000000) << 21; + return (string)($timestampPart | random_int(1, 0x1FFFFF)); +} + +function javaStringHashCode(string $value): int +{ + $hash = 0; + $length = strlen($value); + for ($i = 0; $i < $length; $i++) { + $hash = (31 * $hash + ord($value[$i])) & 0xFFFFFFFF; + if ($hash >= 0x80000000) { + $hash -= 0x100000000; + } + } + + return $hash; +} + +function imUserMessagesTable(string $userId): string +{ + return 't_user_messages_' . (abs(javaStringHashCode($userId)) % 128); +} + +function imMessageTable(string $messageId): string +{ + $timestamp = ((int)$messageId >> 21) + 1514736000000; + $date = (new DateTimeImmutable('@' . (int)floor($timestamp / 1000))) + ->setTimezone(new DateTimeZone(IM_DB_TIMEZONE)); + $year = (int)$date->format('Y'); + $month = (int)$date->format('n') - 1; + + return 't_messages_' . (($year % 3) * 12 + $month); +} diff --git a/dist-local/api/media_download.php b/dist-local/api/media_download.php new file mode 100644 index 0000000..634c71d --- /dev/null +++ b/dist-local/api/media_download.php @@ -0,0 +1,58 @@ + strtolower(rtrim(trim($host), '.')), + preg_split('/[,\s]+/', $configured) ?: [] + ); + + return array_values(array_unique(array_filter($hosts))); +} + +function validatedMediaProxyUrl(string $url): array +{ + if ($url === '' || strlen($url) > 2048 || filter_var($url, FILTER_VALIDATE_URL) === false) { + failMediaProxy(400, '媒体地址不合法'); + } + + $parts = parse_url($url); + $scheme = strtolower((string)($parts['scheme'] ?? '')); + $host = strtolower(rtrim((string)($parts['host'] ?? ''), '.')); + $port = isset($parts['port']) ? (int)$parts['port'] : 443; + if ( + $scheme !== 'https' + || $host === '' + || $port !== 443 + || isset($parts['user']) + || isset($parts['pass']) + || !in_array($host, mediaProxyAllowedHosts(), true) + ) { + failMediaProxy(403, '媒体来源不允许代理'); + } + + return [$url, $host]; +} + +function mediaProxyCacheRoot(): string +{ + $root = rtrim(webImEnv('JC_WEB_IM_MEDIA_CACHE_ROOT', '/var/cache/jc-web-im-media'), '/'); + if ($root === '' || !is_dir($root) || !is_writable($root)) { + failMediaProxy(503, '媒体缓存目录不可用'); + } + + return $root; +} + +function mediaProxyPreferredIps(): array +{ + $configured = preg_split('/[,\s]+/', webImEnv('JC_WEB_IM_MEDIA_PROXY_IPS')) ?: []; + $ips = []; + foreach ($configured as $ip) { + $ip = trim($ip); + if ( + $ip !== '' + && filter_var( + $ip, + FILTER_VALIDATE_IP, + FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE + ) !== false + ) { + $ips[$ip] = $ip; + } + } + + return array_values($ips); +} + +function mediaProxyMime(string $path): string +{ + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = $finfo !== false ? (string)(finfo_file($finfo, $path) ?: '') : ''; + if ($finfo !== false) { + finfo_close($finfo); + } + + return $mime; +} + +function fetchMediaProxyImage(string $url, string $host, string $cacheFile): void +{ + $deadline = microtime(true) + WEB_MEDIA_PROXY_MAX_SECONDS; + $lastError = '媒体源无响应'; + $preferredIps = mediaProxyPreferredIps(); + $targets = array_merge($preferredIps, array_fill(0, max(2, 6 - count($preferredIps)), null)); + + foreach ($targets as $index => $preferredIp) { + if (microtime(true) >= $deadline) { + break; + } + $attempt = $index + 1; + $temporaryFile = $cacheFile . '.tmp.' . getmypid() . '.' . $attempt; + $stream = @fopen($temporaryFile, 'x+b'); + if ($stream === false) { + $lastError = '无法创建媒体缓存文件'; + break; + } + + $bytes = 0; + $tooLarge = false; + $curl = curl_init($url); + if ($curl === false) { + fclose($stream); + @unlink($temporaryFile); + $lastError = '无法初始化媒体请求'; + break; + } + + $remainingMs = max(1000, (int)floor(($deadline - microtime(true)) * 1000)); + $curlOptions = [ + CURLOPT_RETURNTRANSFER => false, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_CONNECTTIMEOUT_MS => min(2500, $remainingMs), + CURLOPT_TIMEOUT_MS => min(12000, $remainingMs), + CURLOPT_NOSIGNAL => true, + CURLOPT_DNS_CACHE_TIMEOUT => 0, + CURLOPT_FRESH_CONNECT => true, + CURLOPT_FORBID_REUSE => true, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_USERAGENT => 'JC-Web-IM-Media-Proxy/1.0', + CURLOPT_HTTPHEADER => ['Accept: image/avif,image/webp,image/*,*/*;q=0.8'], + CURLOPT_WRITEFUNCTION => static function ($handle, string $chunk) use ($stream, &$bytes, &$tooLarge): int { + $length = strlen($chunk); + if ($bytes + $length > WEB_MEDIA_PROXY_MAX_BYTES) { + $tooLarge = true; + return 0; + } + $written = fwrite($stream, $chunk); + if ($written === false) { + return 0; + } + $bytes += $written; + return $written; + }, + ]; + if (is_string($preferredIp) && $preferredIp !== '') { + $curlOptions[CURLOPT_RESOLVE] = [$host . ':443:' . $preferredIp]; + } + curl_setopt_array($curl, $curlOptions); + + $result = curl_exec($curl); + $status = (int)curl_getinfo($curl, CURLINFO_RESPONSE_CODE); + $curlError = curl_error($curl); + curl_close($curl); + fflush($stream); + fclose($stream); + + $mime = $bytes > 0 && is_file($temporaryFile) ? mediaProxyMime($temporaryFile) : ''; + if ($result !== false && $status >= 200 && $status < 300 && !$tooLarge && str_starts_with($mime, 'image/')) { + if (!@rename($temporaryFile, $cacheFile)) { + @unlink($temporaryFile); + $lastError = '媒体缓存保存失败'; + break; + } + @chmod($cacheFile, 0644); + return; + } + + @unlink($temporaryFile); + $lastError = $tooLarge + ? '图片超过代理大小限制' + : ($curlError !== '' ? $curlError : ($status > 0 ? '媒体源返回 HTTP ' . $status : '媒体源无响应')); + if ($attempt < count($targets) && microtime(true) < $deadline) { + usleep(150000); + } + } + + error_log(sprintf('media_proxy fetch failed host=%s error=%s', $host, $lastError)); + failMediaProxy(502, '图片源暂时不可用,请稍后重试'); +} + +function serveMediaProxyImage(string $cacheFile, string $etag): never +{ + if (!is_file($cacheFile)) { + failMediaProxy(404, '媒体缓存不存在'); + } + $mime = mediaProxyMime($cacheFile); + if (!str_starts_with($mime, 'image/')) { + failMediaProxy(415, '媒体内容不是图片'); + } + + header('ETag: "' . $etag . '"'); + header('Cache-Control: public, max-age=86400, immutable'); + header('Content-Type: ' . $mime); + header('Content-Length: ' . (string)filesize($cacheFile)); + header('Content-Disposition: inline'); + header('X-Content-Type-Options: nosniff'); + if (trim((string)($_SERVER['HTTP_IF_NONE_MATCH'] ?? ''), ' W/') === '"' . $etag . '"') { + http_response_code(304); + exit; + } + + readfile($cacheFile); + exit; +} + +[$url, $host] = validatedMediaProxyUrl(trim((string)($_GET['url'] ?? ''))); +$cacheRoot = mediaProxyCacheRoot(); +$cacheKey = hash('sha256', $url); +$cacheFile = $cacheRoot . '/' . $cacheKey . '.image'; +$lockFile = $cacheRoot . '/' . $cacheKey . '.lock'; + +if (!is_file($cacheFile)) { + $lock = @fopen($lockFile, 'c'); + if ($lock === false || !flock($lock, LOCK_EX)) { + if (is_resource($lock)) { + fclose($lock); + } + failMediaProxy(503, '媒体缓存暂时忙碌'); + } + if (!is_file($cacheFile)) { + fetchMediaProxyImage($url, $host, $cacheFile); + } + flock($lock, LOCK_UN); + fclose($lock); +} + +serveMediaProxyImage($cacheFile, $cacheKey); diff --git a/dist-local/api/message_action.php b/dist-local/api/message_action.php new file mode 100644 index 0000000..f633822 --- /dev/null +++ b/dist-local/api/message_action.php @@ -0,0 +1,288 @@ +prepare( + 'select `_uid` from `t_user` where `_uid` = ? or `_mobile` = ? or `_name` = ? limit 1' + ); + $statement->execute([$value, $value, $value]); + $uid = $statement->fetchColumn(); + + return is_string($uid) && $uid !== '' ? $uid : $value; +} + +function messageTimestampMs(string $messageUid): int +{ + return (((int)$messageUid) >> 21) + 1514736000000; +} + +function nowTimestampMs(): int +{ + return (int)floor(microtime(true) * 1000); +} + +function recallLimitMinutes(): float +{ + $statement = imAppDb()->prepare( + 'select `conf_value` from `system_conf`' + . ' where `conf_name` = ? and `is_valid` = 1 limit 1' + ); + $statement->execute(['RetractMessage']); + $value = trim((string)($statement->fetchColumn() ?: '0')); + + if ($value === '' || !is_numeric($value)) { + return 0.0; + } + + return max(0.0, (float)$value); +} + +function assertRecallWithinLimit(string $messageUid): void +{ + $limitMinutes = recallLimitMinutes(); + if ($limitMinutes <= 0) { + return; + } + + $messageAgeMs = nowTimestampMs() - messageTimestampMs($messageUid); + $limitMs = (int)round($limitMinutes * 60 * 1000); + if ($messageAgeMs > $limitMs) { + throw new UserActionException('已超过可撤回时间'); + } +} + +function fetchActionMessageRow(string $messageUid): array +{ + $table = imMessageTable($messageUid); + $statement = imDb()->prepare( + 'select `_mid`, `_from`, `_content_type`, `_searchable_key` from `' . $table . '` where `_mid` = ? limit 1' + ); + $statement->execute([$messageUid]); + $row = $statement->fetch(); + + return is_array($row) ? $row : []; +} + +function ensureMessageEditsTable(): void +{ + imDb()->exec( + 'create table if not exists `t_web_message_edits` (' + . '`message_uid` varchar(32) not null,' + . '`editor_uid` varchar(64) not null,' + . '`edited_text` text null,' + . '`edited_at_ms` bigint not null,' + . '`created_at` datetime not null default current_timestamp,' + . '`updated_at` datetime null default current_timestamp on update current_timestamp,' + . 'primary key (`message_uid`),' + . 'key `idx_editor_uid` (`editor_uid`),' + . 'key `idx_edited_at_ms` (`edited_at_ms`)' + . ') engine=InnoDB default charset=utf8mb4' + ); +} + +function editTextMessage(string $operatorId, string $messageUid, string $text): array +{ + $row = fetchActionMessageRow($messageUid); + if (!$row) { + return editWebGroupMessage($operatorId, $messageUid, $text); + } + if ((string)$row['_from'] !== $operatorId) { + throw new UserActionException('只能编辑自己发送的消息'); + } + if ((int)$row['_content_type'] !== 1) { + throw new UserActionException('只能编辑文字消息'); + } + + $editedAt = nowTimestampMs(); + $table = imMessageTable($messageUid); + $update = imDb()->prepare('update `' . $table . '` set `_searchable_key` = ? where `_mid` = ?'); + $update->execute([$text, $messageUid]); + + ensureMessageEditsTable(); + $upsert = imDb()->prepare( + 'insert into `t_web_message_edits` (`message_uid`, `editor_uid`, `edited_text`, `edited_at_ms`)' + . ' values (?, ?, ?, ?)' + . ' on duplicate key update' + . ' `editor_uid` = values(`editor_uid`),' + . ' `edited_text` = values(`edited_text`),' + . ' `edited_at_ms` = values(`edited_at_ms`),' + . ' `updated_at` = current_timestamp' + ); + $upsert->execute([$messageUid, $operatorId, $text, $editedAt]); + + return [ + 'messageUid' => $messageUid, + 'text' => $text, + 'editedAt' => $editedAt, + ]; +} + +function fetchWebGroupActionMessage(string $messageUid): array +{ + ensureWebImAppTables(); + $statement = imAppDb()->prepare( + 'select m.*, g.`category` from `t_web_group_messages` m' + . ' join `t_web_groups` g on g.`group_id` = m.`group_id` and g.`deleted` = 0' + . ' where m.`message_uid` = ? limit 1' + ); + $statement->execute([$messageUid]); + $row = $statement->fetch(); + + return is_array($row) ? $row : []; +} + +function editWebGroupMessage(string $operatorId, string $messageUid, string $text): array +{ + $row = fetchWebGroupActionMessage($messageUid); + if (!$row) { + throw new UserActionException('消息不存在'); + } + if ((string)$row['sender_uid'] !== $operatorId) { + throw new UserActionException('只能编辑自己发送的消息'); + } + if ((int)($row['recalled'] ?? 0) !== 0) { + throw new UserActionException('已撤回的消息请重新编辑发送'); + } + + $editedAt = nowTimestampMs(); + $update = imAppDb()->prepare( + 'update `t_web_group_messages` set `text` = ?, `edited_at_ms` = ? where `message_uid` = ?' + ); + $update->execute([$text, $editedAt, $messageUid]); + $groupUpdate = imAppDb()->prepare('update `t_web_groups` set `updated_at_ms` = ? where `group_id` = ?'); + $groupUpdate->execute([$editedAt, (string)$row['group_id']]); + + return [ + 'messageUid' => $messageUid, + 'text' => $text, + 'editedAt' => $editedAt, + ]; +} + +function webGroupMemberRoleForAction(string $groupId, string $userId): string +{ + $statement = imAppDb()->prepare( + 'select `role` from `t_web_group_members`' + . ' where `group_id` = ? and `user_id` = ? and `status` = 0 limit 1' + ); + $statement->execute([$groupId, $userId]); + $role = $statement->fetchColumn(); + + return is_string($role) ? $role : ''; +} + +function recallWebGroupMessage(string $operatorId, string $messageUid): bool +{ + $row = fetchWebGroupActionMessage($messageUid); + if (!$row) { + return false; + } + $isSender = (string)$row['sender_uid'] === $operatorId; + if (!$isSender) { + $role = webGroupMemberRoleForAction((string)$row['group_id'], $operatorId); + if (!in_array($role, ['owner', 'admin'], true)) { + throw new UserActionException('只有群主或管理员可以撤回他人消息'); + } + } + assertRecallWithinLimit($messageUid); + + $now = nowTimestampMs(); + $update = imAppDb()->prepare( + 'update `t_web_group_messages` set `recalled` = 1 where `message_uid` = ?' + ); + $update->execute([$messageUid]); + $groupUpdate = imAppDb()->prepare('update `t_web_groups` set `updated_at_ms` = ? where `group_id` = ?'); + $groupUpdate->execute([$now, (string)$row['group_id']]); + + return true; +} + +$payload = readJsonPayload(); +$action = trim((string)($payload['action'] ?? '')); +$userId = trim((string)($payload['userId'] ?? '')); +$messageUidText = trim((string)($payload['messageUid'] ?? '')); +$messageUid = ctype_digit($messageUidText) ? (int)$messageUidText : 0; + +if ($action === '' || $messageUid <= 0) { + respond(400, '缺少操作类型或消息ID'); +} + +try { + if ($action === 'recall') { + if ($userId === '') { + respond(400, '缺少操作者ID'); + } + $userId = resolveActionUserId($userId); + $row = fetchActionMessageRow($messageUidText); + if (!$row && recallWebGroupMessage($userId, $messageUidText)) { + respond(0, 'success', [ + 'messageUid' => (string)$messageUid, + 'action' => $action, + ]); + } + if (!$row) { + throw new UserActionException('消息不存在'); + } + assertRecallWithinLimit($messageUidText); + requireImSuccess(imAdminPost('/admin/message/recall', [ + 'operator' => $userId, + 'messageUid' => $messageUid, + 'userRecall' => true, + 'isUserRecall' => true, + ]), '撤回失败'); + + respond(0, 'success', [ + 'messageUid' => (string)$messageUid, + 'action' => $action, + ]); + } + + if ($action === 'edit') { + if ($userId === '') { + respond(400, '缺少操作者ID'); + } + $text = trim((string)($payload['text'] ?? '')); + if ($text === '') { + respond(400, '编辑内容不能为空'); + } + if (mb_strlen($text, 'UTF-8') > 5000) { + respond(400, '编辑内容过长'); + } + + $userId = resolveActionUserId($userId); + respond(0, 'success', [ + 'message' => editTextMessage($userId, $messageUidText, $text), + 'action' => $action, + ]); + } + + if ($action === 'delete') { + requireImSuccess(imAdminPost('/admin/message/delete', [ + 'messageUid' => $messageUid, + ]), '删除失败'); + + respond(0, 'success', [ + 'messageUid' => (string)$messageUid, + 'action' => $action, + ]); + } + + respond(400, '不支持的消息操作'); +} catch (Throwable $error) { + respond($error instanceof UserActionException ? 400 : 500, $error->getMessage() ?: '消息操作失败'); +} diff --git a/dist-local/api/message_detail.php b/dist-local/api/message_detail.php new file mode 100644 index 0000000..06454bf --- /dev/null +++ b/dist-local/api/message_detail.php @@ -0,0 +1,387 @@ + 0) { + return ($id >> 21) + 1514736000000; + } + + if ($datetime !== null && $datetime !== '') { + $date = DateTimeImmutable::createFromFormat( + 'Y-m-d H:i:s', + $datetime, + new DateTimeZone(IM_DB_TIMEZONE) + ); + if ($date instanceof DateTimeImmutable) { + return $date->getTimestamp() * 1000; + } + } + + return 0; +} + +function detailResolveUserId(PDO $db, string $value): string +{ + if ($value === '') { + return ''; + } + + $statement = $db->prepare( + 'select `_uid` from `t_user` where `_uid` = ? or `_mobile` = ? or `_name` = ? limit 1' + ); + $statement->execute([$value, $value, $value]); + $uid = $statement->fetchColumn(); + + return is_string($uid) && $uid !== '' ? $uid : $value; +} + +function detailConversationType(mixed $value): string +{ + $type = strtolower(trim((string)$value)); + return in_array($type, ['1', 'group', 'channel'], true) ? 'group' : 'single'; +} + +function detailDeliveryMap(PDO $db, array $userIds): array +{ + $userIds = array_values(array_unique(array_filter(array_map('strval', $userIds)))); + if (!$userIds) { + return []; + } + + $statement = $db->prepare( + 'select `_uid`, max(`_dt`) as `_dt` from `t_delivery_report`' + . ' where `_uid` in (' . detailPlaceholders($userIds) . ') group by `_uid`' + ); + $statement->execute($userIds); + + $items = []; + foreach ($statement->fetchAll() as $row) { + $items[(string)$row['_uid']] = (int)$row['_dt']; + } + + return $items; +} + +function detailReadAt(PDO $db, string $readerId, array $targets): int +{ + $targets = array_values(array_unique(array_filter(array_map('strval', $targets)))); + if ($readerId === '' || !$targets) { + return 0; + } + + $statement = $db->prepare( + 'select max(`_dt`) from `t_read_report`' + . ' where `_uid` = ? and `_type` = 0 and `_line` = 0' + . ' and `_target` in (' . detailPlaceholders($targets) . ')' + ); + $statement->execute(array_merge([$readerId], $targets)); + $value = $statement->fetchColumn(); + + return is_numeric($value) ? (int)$value : 0; +} + +function detailSingleMessageVisible(PDO $db, string $userId, string $targetId, string $messageUid): bool +{ + $table = imUserMessagesTable($userId); + $conversationKeys = [$userId . '|' . $targetId, $targetId . '|' . $userId]; + $statement = $db->prepare( + 'select 1 from `' . $table . '`' + . ' where `_uid` = ? and `_mid` = ? and `_type` = 0 and `_line` = 0' + . ' and `_target` in (' . detailPlaceholders($conversationKeys) . ') limit 1' + ); + $statement->execute(array_merge([$userId, $messageUid], $conversationKeys)); + + return (bool)$statement->fetchColumn(); +} + +function detailUserMap(PDO $imDb, PDO $appDb, array $userIds, string $ownerId, string $groupId = ''): array +{ + $userIds = array_values(array_unique(array_filter(array_map('strval', $userIds)))); + if (!$userIds) { + return []; + } + + $statement = $imDb->prepare( + 'select `_uid`, `_name`, `_display_name`, `_mobile`, `_portrait` from `t_user`' + . ' where `_uid` in (' . detailPlaceholders($userIds) . ')' + ); + $statement->execute($userIds); + + $users = []; + foreach ($statement->fetchAll() as $row) { + $uid = (string)$row['_uid']; + $rawName = trim((string)($row['_display_name'] ?? '')) + ?: trim((string)($row['_name'] ?? '')) + ?: trim((string)($row['_mobile'] ?? '')) + ?: $uid; + $users[$uid] = [ + 'uid' => $uid, + 'rawName' => $rawName, + 'mobile' => (string)($row['_mobile'] ?? ''), + 'portrait' => (string)($row['_portrait'] ?? ''), + 'contactAlias' => '', + 'remarkAlias' => '', + 'groupAlias' => '', + 'role' => '', + 'displayName' => $rawName, + ]; + } + + if ($ownerId !== '') { + $aliasStatement = $imDb->prepare( + 'select `_friend_uid`, `_alias` from `t_friend` where `_uid` = ?' + . ' and `_friend_uid` in (' . detailPlaceholders($userIds) . ')' + ); + $aliasStatement->execute(array_merge([$ownerId], $userIds)); + foreach ($aliasStatement->fetchAll() as $row) { + $uid = (string)$row['_friend_uid']; + if (isset($users[$uid])) { + $users[$uid]['contactAlias'] = trim((string)($row['_alias'] ?? '')); + } + } + + try { + $remarkMap = fetchWebUserRemarkMap($appDb, $ownerId, $userIds); + } catch (Throwable) { + $remarkMap = []; + } + foreach ($remarkMap as $uid => $alias) { + if (isset($users[$uid])) { + $users[$uid]['remarkAlias'] = trim((string)$alias); + } + } + } + + if ($groupId !== '') { + $memberStatement = $appDb->prepare( + 'select `user_id`, `alias`, `role` from `t_web_group_members`' + . ' where `group_id` = ? and `user_id` in (' . detailPlaceholders($userIds) . ')' + ); + $memberStatement->execute(array_merge([$groupId], $userIds)); + foreach ($memberStatement->fetchAll() as $row) { + $uid = (string)$row['user_id']; + if (isset($users[$uid])) { + $users[$uid]['groupAlias'] = trim((string)($row['alias'] ?? '')); + $users[$uid]['role'] = (string)($row['role'] ?? ''); + } + } + } + + foreach ($users as $uid => $user) { + $alias = (string)$user['contactAlias'] !== '' + ? (string)$user['contactAlias'] + : ((string)$user['remarkAlias'] !== '' ? (string)$user['remarkAlias'] : (string)$user['groupAlias']); + $users[$uid]['displayName'] = $alias !== '' ? $alias : (string)$user['rawName']; + } + + foreach ($userIds as $uid) { + if (!isset($users[$uid])) { + $users[$uid] = [ + 'uid' => $uid, + 'rawName' => $uid, + 'mobile' => '', + 'portrait' => '', + 'contactAlias' => '', + 'remarkAlias' => '', + 'groupAlias' => '', + 'role' => '', + 'displayName' => $uid, + ]; + } + } + + return $users; +} + +function detailMember(array $user, int $time = 0): array +{ + return [ + 'uid' => (string)$user['uid'], + 'name' => (string)$user['displayName'], + 'rawName' => (string)$user['rawName'], + 'mobile' => (string)($user['mobile'] ?? ''), + 'portrait' => (string)($user['portrait'] ?? ''), + 'role' => (string)($user['role'] ?? ''), + 'time' => $time, + ]; +} + +function singleDetail(PDO $imDb, string $userId, string $targetId, string $messageUid): array +{ + if (!preg_match('/^\d+$/', $messageUid)) { + respond(400, '缺少可查询的消息ID'); + } + + $table = imMessageTable($messageUid); + $statement = $imDb->prepare( + 'select `_mid`, `_from`, `_target`, `_dt`, `_content_type`, `_searchable_key`, `_to`' + . ' from `' . $table . '` where `_mid` = ? limit 1' + ); + $statement->execute([$messageUid]); + $message = $statement->fetch(); + if (!is_array($message)) { + respond(404, '没有找到该消息'); + } + + $senderId = (string)$message['_from']; + if (!detailSingleMessageVisible($imDb, $userId, $targetId, $messageUid)) { + respond(403, '没有权限查看该消息'); + } + + $receiverId = $senderId === $userId ? $targetId : $userId; + if ($senderId !== $userId && $senderId !== $targetId) { + respond(403, '没有权限查看该消息'); + } + + $sentAt = detailTimestampMs((string)($message['_dt'] ?? ''), $messageUid); + $targets = [$senderId, $senderId . '|' . $receiverId, $receiverId . '|' . $senderId]; + $deliveredRaw = detailDeliveryMap($imDb, [$receiverId])[$receiverId] ?? 0; + $readRaw = detailReadAt($imDb, $receiverId, $targets); + $users = detailUserMap($imDb, imAppDb(), [$senderId, $receiverId], $userId); + + return [ + 'messageUid' => $messageUid, + 'conversationType' => 'single', + 'sentAt' => $sentAt, + 'sender' => detailMember($users[$senderId]), + 'receiver' => detailMember($users[$receiverId]), + 'deliveredAt' => $deliveredRaw >= $sentAt ? $deliveredRaw : 0, + 'readAt' => $readRaw >= $sentAt ? $readRaw : 0, + ]; +} + +function groupDetail(PDO $imDb, PDO $appDb, string $userId, string $groupId, string $messageUid): array +{ + ensureWebImAppTables(); + if (!preg_match('/^\d+$/', $messageUid)) { + respond(400, '缺少可查询的消息ID'); + } + + $myMember = $appDb->prepare( + 'select `role` from `t_web_group_members` where `group_id` = ? and `user_id` = ? and `status` = 0 limit 1' + ); + $myMember->execute([$groupId, $userId]); + if (!$myMember->fetchColumn()) { + respond(403, '你不在该群组中'); + } + + $messageStatement = $appDb->prepare( + 'select `message_uid`, `group_id`, `sender_uid`, `created_at_ms`, `edited_at_ms`, `recalled`' + . ' from `t_web_group_messages` where `group_id` = ? and `message_uid` = ? limit 1' + ); + $messageStatement->execute([$groupId, $messageUid]); + $message = $messageStatement->fetch(); + if (!is_array($message)) { + respond(404, '没有找到该群消息'); + } + + $sentAt = (int)$message['created_at_ms']; + $editedAt = (int)($message['edited_at_ms'] ?? 0); + $activityAt = max($sentAt, $editedAt); + $senderId = (string)$message['sender_uid']; + + $memberStatement = $appDb->prepare( + 'select `user_id`, `role`, `alias`, `joined_at_ms` from `t_web_group_members`' + . ' where `group_id` = ? and `status` = 0 and `joined_at_ms` <= ?' + . ' order by (`role` = "owner") desc, (`role` = "admin") desc, `joined_at_ms` asc' + ); + $memberStatement->execute([$groupId, $sentAt]); + $members = array_values(array_filter($memberStatement->fetchAll(), static function (array $member) use ($senderId): bool { + return (string)$member['user_id'] !== $senderId; + })); + + $memberIds = array_values(array_unique(array_map(static fn(array $member): string => (string)$member['user_id'], $members))); + $users = detailUserMap($imDb, $appDb, array_merge([$senderId], $memberIds), $userId, $groupId); + $deliveryMap = detailDeliveryMap($imDb, $memberIds); + + $readMap = []; + if ($memberIds) { + $readStatement = $appDb->prepare( + 'select `user_id`, `read_at_ms` from `t_web_group_reads`' + . ' where `group_id` = ? and `user_id` in (' . detailPlaceholders($memberIds) . ')' + ); + $readStatement->execute(array_merge([$groupId], $memberIds)); + foreach ($readStatement->fetchAll() as $row) { + $readMap[(string)$row['user_id']] = (int)$row['read_at_ms']; + } + } + + $read = []; + $unread = []; + $undelivered = []; + foreach ($memberIds as $uid) { + $readAt = (int)($readMap[$uid] ?? 0); + $deliveredAt = (int)($deliveryMap[$uid] ?? 0); + $member = detailMember($users[$uid], $readAt >= $activityAt ? $readAt : ($deliveredAt >= $sentAt ? $deliveredAt : 0)); + if ($readAt >= $activityAt) { + $read[] = $member; + } elseif ($deliveredAt >= $sentAt) { + $unread[] = $member; + } else { + $undelivered[] = $member; + } + } + + return [ + 'messageUid' => $messageUid, + 'conversationType' => 'group', + 'groupId' => $groupId, + 'sentAt' => $sentAt, + 'editedAt' => $editedAt, + 'recalled' => (bool)$message['recalled'], + 'sender' => detailMember($users[$senderId] ?? ['uid' => $senderId, 'displayName' => $senderId, 'rawName' => $senderId]), + 'group' => [ + 'read' => $read, + 'unread' => $unread, + 'undelivered' => $undelivered, + 'counts' => [ + 'read' => count($read), + 'unread' => count($unread), + 'undelivered' => count($undelivered), + 'total' => count($memberIds), + ], + ], + ]; +} + +$payload = readJsonPayload(); +$userId = trim((string)($payload['userId'] ?? '')); +$target = trim((string)($payload['target'] ?? '')); +$messageUid = trim((string)($payload['messageUid'] ?? $payload['messageId'] ?? '')); +$conversationType = detailConversationType($payload['conversationType'] ?? $payload['type'] ?? 'single'); + +if ($userId === '') { + respond(400, '缺少用户ID'); +} +if ($target === '') { + respond(400, '缺少会话对象'); +} +if ($messageUid === '') { + respond(400, '缺少消息ID'); +} + +try { + $imDb = imDb(); + $appDb = imAppDb(); + $userId = detailResolveUserId($imDb, $userId); + if ($conversationType === 'group') { + respond(0, 'success', ['detail' => groupDetail($imDb, $appDb, $userId, $target, $messageUid)]); + } + + $targetId = detailResolveUserId($imDb, $target); + respond(0, 'success', ['detail' => singleDetail($imDb, $userId, $targetId, $messageUid)]); +} catch (Throwable $error) { + respond(500, $error->getMessage() ?: '消息详情加载失败'); +} diff --git a/dist-local/api/messages.php b/dist-local/api/messages.php new file mode 100644 index 0000000..ac03851 --- /dev/null +++ b/dist-local/api/messages.php @@ -0,0 +1,900 @@ + 0) { + return ($id >> 21) + 1514736000000; + } + + if ($datetime !== null && $datetime !== '') { + $date = DateTimeImmutable::createFromFormat( + 'Y-m-d H:i:s', + $datetime, + new DateTimeZone(IM_DB_TIMEZONE) + ); + if ($date instanceof DateTimeImmutable) { + return $date->getTimestamp() * 1000; + } + } + + return (int)floor(microtime(true) * 1000); +} + +function nowMs(): int +{ + return (int)floor(microtime(true) * 1000); +} + +function markUserDelivered(PDO $db, string $userId, int $timestamp): void +{ + if ($userId === '' || $timestamp <= 0) { + return; + } + + $statement = $db->prepare( + 'insert into `t_delivery_report` (`_uid`, `_dt`) values (?, ?)' + . ' on duplicate key update `_dt` = if(`_dt` < values(`_dt`), values(`_dt`), `_dt`)' + ); + $statement->execute([$userId, $timestamp]); +} + +function markConversationRead(PDO $db, string $userId, string $targetId, int $timestamp): void +{ + if ($userId === '' || $targetId === '' || $timestamp <= 0) { + return; + } + + $statement = $db->prepare( + 'select `id`, `_dt` from `t_read_report`' + . ' where `_uid` = ? and `_type` = 0 and `_line` = 0 and `_target` = ?' + . ' order by `_dt` desc limit 1' + ); + $statement->execute([$userId, $targetId]); + $row = $statement->fetch(); + + if ($row) { + if ($timestamp > (int)$row['_dt']) { + $update = $db->prepare('update `t_read_report` set `_dt` = ? where `id` = ?'); + $update->execute([$timestamp, (int)$row['id']]); + } + return; + } + + $insert = $db->prepare( + 'insert into `t_read_report` (`_uid`, `_type`, `_line`, `_target`, `_dt`) values (?, 0, 0, ?, ?)' + ); + $insert->execute([$userId, $targetId, $timestamp]); +} + +function resolveUserId(PDO $db, string $value): string +{ + if ($value === '') { + return ''; + } + + $statement = $db->prepare( + 'select `_uid` from `t_user` where `_uid` = ? or `_mobile` = ? or `_name` = ? limit 1' + ); + $statement->execute([$value, $value, $value]); + $uid = $statement->fetchColumn(); + + return is_string($uid) && $uid !== '' ? $uid : $value; +} + +function peerFromConversationTarget(string $userId, string $target): string +{ + $parts = array_values(array_filter(explode('|', $target), static fn($part) => $part !== '')); + if (count($parts) < 2) { + return $target !== $userId ? $target : ''; + } + + return $parts[0] === $userId ? $parts[1] : $parts[0]; +} + +function fetchMessageRows(PDO $db, array $messageIds): array +{ + $messageIds = array_values(array_unique(array_filter(array_map('strval', $messageIds)))); + if (!$messageIds) { + return []; + } + + $idsByTable = []; + foreach ($messageIds as $messageId) { + $idsByTable[imMessageTable($messageId)][] = $messageId; + } + + $messages = []; + foreach ($idsByTable as $table => $ids) { + $sql = 'select `_mid`, `_from`, `_type`, `_target`, `_line`, `_searchable_key`, `_dt`, `_content_type`, `_to`' + . ' from `' . $table . '` where `_mid` in (' . placeholders($ids) . ')'; + $statement = $db->prepare($sql); + $statement->execute($ids); + foreach ($statement->fetchAll() as $row) { + $messages[(string)$row['_mid']] = $row; + } + } + + return $messages; +} + +function fetchMessageEditMap(PDO $db, array $messageIds): array +{ + $messageIds = array_values(array_unique(array_filter(array_map('strval', $messageIds)))); + if (!$messageIds) { + return []; + } + + try { + $statement = $db->prepare( + 'select `message_uid`, `edited_at_ms` from `t_web_message_edits`' + . ' where `message_uid` in (' . placeholders($messageIds) . ')' + ); + $statement->execute($messageIds); + } catch (Throwable) { + return []; + } + + $edits = []; + foreach ($statement->fetchAll() as $row) { + $edits[(string)$row['message_uid']] = [ + 'editedAt' => (int)$row['edited_at_ms'], + ]; + } + + return $edits; +} + +function normalizeAttachmentRow(array $row): array +{ + return [ + 'type' => (string)$row['attachment_type'], + 'url' => (string)$row['url'], + 'name' => (string)($row['name'] ?? ''), + 'mime' => (string)($row['mime'] ?? ''), + 'size' => (int)($row['size_bytes'] ?? 0), + ]; +} + +function fetchAttachmentMap(PDO $appDb, array $messageIds): array +{ + $messageIds = array_values(array_unique(array_filter(array_map('strval', $messageIds)))); + if (!$messageIds) { + return []; + } + + ensureWebImAppTables(); + $statement = $appDb->prepare( + 'select `message_uid`, `attachment_type`, `url`, `name`, `mime`, `size_bytes`' + . ' from `t_web_message_attachments`' + . ' where `message_uid` in (' . placeholders($messageIds) . ')' + ); + $statement->execute($messageIds); + + $attachments = []; + foreach ($statement->fetchAll() as $row) { + $attachments[(string)$row['message_uid']] = normalizeAttachmentRow($row); + } + + return $attachments; +} + +function attachmentFallbackText(?array $attachment): string +{ + if (!$attachment) { + return ''; + } + if ((string)$attachment['type'] === 'image') { + return '[图片]'; + } + + return '[文件] ' . ((string)($attachment['name'] ?? '') ?: '文件'); +} + +function attachmentDisplayText(string $text, ?array $attachment): string +{ + if (!$attachment) { + return $text; + } + + return $text === attachmentFallbackText($attachment) ? '' : $text; +} + +function messagePreviewText(?array $message): string +{ + if (!$message) { + return ''; + } + $attachment = is_array($message['attachment'] ?? null) ? $message['attachment'] : null; + if ($attachment) { + $text = trim((string)($message['text'] ?? '')); + return $text !== '' ? $text : attachmentFallbackText($attachment); + } + + return (string)($message['text'] ?? ''); +} + +function fetchUserInfoMap(PDO $db, array $userIds, string $ownerId = ''): array +{ + $userIds = array_values(array_unique(array_filter(array_map('strval', $userIds)))); + if (!$userIds) { + return []; + } + + $statement = $db->prepare( + 'select `_uid`, `_name`, `_display_name`, `_mobile`, `_portrait`, `_address` from `t_user`' + . ' where `_uid` in (' . placeholders($userIds) . ')' + ); + $statement->execute($userIds); + + $users = []; + foreach ($statement->fetchAll() as $row) { + $row['_is_friend'] = false; + $row['_alias'] = ''; + $row['_friend_alias'] = ''; + $row['_remark_alias'] = ''; + $users[(string)$row['_uid']] = $row; + } + + if ($ownerId !== '') { + $aliasStatement = $db->prepare( + 'select `_friend_uid`, `_alias` from `t_friend` where `_uid` = ?' + . ' and `_friend_uid` in (' . placeholders($userIds) . ')' + ); + $aliasStatement->execute(array_merge([$ownerId], $userIds)); + foreach ($aliasStatement->fetchAll() as $row) { + $friendUid = (string)$row['_friend_uid']; + if (!isset($users[$friendUid])) { + continue; + } + $users[$friendUid]['_is_friend'] = true; + $users[$friendUid]['_friend_alias'] = trim((string)($row['_alias'] ?? '')); + } + + try { + ensureWebImAppTables(); + $remarkMap = fetchWebUserRemarkMap(imAppDb(), $ownerId, $userIds); + } catch (Throwable) { + $remarkMap = []; + } + foreach ($remarkMap as $targetUid => $alias) { + if (!isset($users[$targetUid])) { + continue; + } + $users[$targetUid]['_remark_alias'] = $alias; + } + + foreach ($users as $uid => $user) { + $friendAlias = trim((string)($user['_friend_alias'] ?? '')); + $remarkAlias = trim((string)($user['_remark_alias'] ?? '')); + $users[$uid]['_alias'] = $friendAlias !== '' ? $friendAlias : $remarkAlias; + } + } + + return $users; +} + +function userTitle(array $users, string $userId): string +{ + $user = $users[$userId] ?? null; + if (!$user) { + return $userId; + } + + return (string)($user['_alias'] ?? '') + ?: (string)($user['_display_name'] ?? '') + ?: (string)($user['_name'] ?? '') + ?: (string)($user['_mobile'] ?? '') + ?: $userId; +} + +function loadPeerReceiptState(PDO $db, string $currentUserId, string $peerId): array +{ + $deliveryStatement = $db->prepare('select `_dt` from `t_delivery_report` where `_uid` = ? limit 1'); + $deliveryStatement->execute([$peerId]); + $deliveredAt = $deliveryStatement->fetchColumn(); + + $readTargets = array_values(array_unique([ + $currentUserId, + $currentUserId . '|' . $peerId, + $peerId . '|' . $currentUserId, + ])); + $readStatement = $db->prepare( + 'select max(`_dt`) from `t_read_report`' + . ' where `_uid` = ? and `_type` = 0 and `_line` = 0' + . ' and `_target` in (' . placeholders($readTargets) . ')' + ); + $readStatement->execute(array_merge([$peerId], $readTargets)); + $readAt = $readStatement->fetchColumn(); + + return [ + 'deliveredAt' => is_numeric($deliveredAt) ? (int)$deliveredAt : 0, + 'readAt' => is_numeric($readAt) ? (int)$readAt : 0, + ]; +} + +function loadOwnReadStateMap(PDO $db, string $userId, array $peerIds): array +{ + $peerIds = array_values(array_unique(array_filter(array_map('strval', $peerIds)))); + if (!$peerIds) { + return []; + } + + $statement = $db->prepare( + 'select `_target`, max(`_dt`) as `_read_at` from `t_read_report`' + . ' where `_uid` = ? and `_type` = 0 and `_line` = 0' + . ' and `_target` in (' . placeholders($peerIds) . ')' + . ' group by `_target`' + ); + $statement->execute(array_merge([$userId], $peerIds)); + + $readAtByPeer = []; + foreach ($statement->fetchAll() as $row) { + $readAtByPeer[(string)$row['_target']] = (int)$row['_read_at']; + } + + return $readAtByPeer; +} + +function outgoingDeliveryStatus(int $timestamp, array $receiptState): string +{ + $readAt = (int)($receiptState['readAt'] ?? 0); + if ($readAt > 0 && $timestamp <= $readAt) { + return 'read'; + } + + $deliveredAt = (int)($receiptState['deliveredAt'] ?? 0); + if ($deliveredAt > 0 && $timestamp <= $deliveredAt) { + return 'delivered'; + } + + return 'sent'; +} + +function messageActivityTimestamp(int $timestamp, array $editState = []): int +{ + return max($timestamp, (int)($editState['editedAt'] ?? 0)); +} + +function normalizeConversationType(mixed $value): string +{ + $type = strtolower(trim((string)$value)); + if (in_array($type, ['1', 'group', 'channel'], true)) { + return 'group'; + } + + return 'single'; +} + +function isWebGroupTarget(PDO $appDb, string $targetId): bool +{ + if ($targetId === '') { + return false; + } + + ensureWebImAppTables(); + $statement = $appDb->prepare('select 1 from `t_web_groups` where `group_id` = ? and `deleted` = 0 limit 1'); + $statement->execute([$targetId]); + + return (bool)$statement->fetchColumn(); +} + +function markWebGroupRead(PDO $appDb, string $userId, string $groupId, int $timestamp): void +{ + if ($userId === '' || $groupId === '' || $timestamp <= 0) { + return; + } + + $now = nowMs(); + $statement = $appDb->prepare( + 'insert into `t_web_group_reads` (`group_id`, `user_id`, `read_at_ms`, `updated_at_ms`)' + . ' values (?, ?, ?, ?)' + . ' on duplicate key update' + . ' `read_at_ms` = if(`read_at_ms` < values(`read_at_ms`), values(`read_at_ms`), `read_at_ms`),' + . ' `updated_at_ms` = values(`updated_at_ms`)' + ); + $statement->execute([$groupId, $userId, $timestamp, $now]); +} + +function webGroupRole(PDO $appDb, string $userId, string $groupId): string +{ + $statement = $appDb->prepare( + 'select `role` from `t_web_group_members`' + . ' where `group_id` = ? and `user_id` = ? and `status` = 0 limit 1' + ); + $statement->execute([$groupId, $userId]); + $role = $statement->fetchColumn(); + + return is_string($role) && $role !== '' ? $role : ''; +} + +function formatWebGroupMessage( + array $row, + string $currentUserId, + array $group, + ?array $attachment = null, + array $users = [] +): array +{ + $groupId = (string)$row['group_id']; + $from = (string)$row['sender_uid']; + $senderUser = $users[$from] ?? []; + $senderAlias = trim((string)($row['sender_alias'] ?? '')); + $senderContactAlias = trim((string)($senderUser['_alias'] ?? '')); + $senderFriendAlias = trim((string)($senderUser['_friend_alias'] ?? '')); + $senderRemarkAlias = trim((string)($senderUser['_remark_alias'] ?? '')); + $senderRawName = trim((string)($senderUser['_display_name'] ?? '')) + ?: trim((string)($senderUser['_name'] ?? '')) + ?: trim((string)($senderUser['_mobile'] ?? '')) + ?: $from; + $senderName = $senderContactAlias + ?: $senderAlias + ?: $senderRawName; + $senderRole = (string)($row['sender_role'] ?? 'member'); + if (!in_array($senderRole, ['owner', 'admin'], true)) { + $senderRole = 'member'; + } + $timestamp = (int)$row['created_at_ms']; + $editedAt = (int)($row['edited_at_ms'] ?? 0); + $activityTimestamp = max($timestamp, $editedAt); + $direction = $from === $currentUserId ? 'out' : 'in'; + + $messageType = $attachment ? (string)$attachment['type'] : 'text'; + $contentType = $messageType === 'image' ? 3 : ($messageType === 'file' ? 5 : 1); + return [ + 'id' => (string)$row['message_uid'], + 'messageUid' => (string)$row['message_uid'], + 'conversationId' => 'group-' . $groupId, + 'from' => $from, + 'senderRole' => $senderRole, + 'senderName' => $senderName, + 'senderPortrait' => (string)($senderUser['_portrait'] ?? ''), + 'senderProfile' => [ + 'uid' => $from, + 'displayName' => $senderName, + 'rawName' => $senderRawName, + 'contactAlias' => $senderContactAlias, + 'friendAlias' => $senderFriendAlias, + 'remarkAlias' => $senderRemarkAlias, + 'groupAlias' => $senderAlias, + 'isContact' => (bool)($senderUser['_is_friend'] ?? false), + 'mobile' => (string)($senderUser['_mobile'] ?? ''), + 'address' => (string)($senderUser['_address'] ?? ''), + 'portrait' => (string)($senderUser['_portrait'] ?? ''), + 'role' => $senderRole, + ], + 'to' => $groupId, + 'target' => $groupId, + 'direction' => $direction, + 'type' => $messageType, + 'contentType' => $contentType, + 'text' => attachmentDisplayText((string)$row['text'], $attachment), + 'attachment' => $attachment, + 'status' => $direction === 'out' ? 'sent' : 'sent', + 'deliveryStatus' => $direction === 'out' ? 'sent' : '', + 'recalled' => false, + 'recallStatus' => '', + 'editedAt' => $editedAt, + 'timestamp' => $timestamp, + 'activityTimestamp' => $activityTimestamp, + 'displayTimestamp' => $timestamp, + 'conversationType' => (string)($group['category'] ?? 'group'), + ]; +} + +function loadWebGroupMessages(PDO $appDb, string $userId, string $groupId, int $limit, bool $markRead): array +{ + ensureWebImAppTables(); + $role = webGroupRole($appDb, $userId, $groupId); + if ($role === '') { + return []; + } + + $groupStatement = $appDb->prepare('select * from `t_web_groups` where `group_id` = ? and `deleted` = 0 limit 1'); + $groupStatement->execute([$groupId]); + $group = $groupStatement->fetch(); + if (!$group) { + return []; + } + + $statement = $appDb->prepare( + 'select msg.*,' + . ' coalesce(sender_member.`role`, if(g.`owner_uid` = msg.`sender_uid`, "owner", "member")) as `sender_role`,' + . ' coalesce(sender_member.`alias`, "") as `sender_alias`' + . ' from `t_web_group_messages` msg' + . ' join `t_web_groups` g on g.`group_id` = msg.`group_id` and g.`deleted` = 0' + . ' left join `t_web_group_members` sender_member' + . ' on sender_member.`group_id` = msg.`group_id`' + . ' and sender_member.`user_id` = msg.`sender_uid`' + . ' and sender_member.`status` = 0' + . ' where msg.`group_id` = ? and msg.`recalled` = 0' + . ' order by msg.`created_at_ms` desc, msg.`id` desc limit ' . $limit + ); + $statement->execute([$groupId]); + $rows = array_reverse($statement->fetchAll()); + $attachmentMap = fetchAttachmentMap($appDb, array_column($rows, 'message_uid')); + $users = fetchUserInfoMap(imDb(), array_column($rows, 'sender_uid'), $userId); + + $readTimestamp = 0; + $messages = []; + foreach ($rows as $row) { + $messageId = (string)$row['message_uid']; + $message = formatWebGroupMessage($row, $userId, $group, $attachmentMap[$messageId] ?? null, $users); + $readTimestamp = max($readTimestamp, (int)$message['activityTimestamp']); + $messages[] = $message; + } + + if ($markRead) { + markWebGroupRead($appDb, $userId, $groupId, $readTimestamp); + } + + return $messages; +} + +function loadWebGroupConversations(PDO $appDb, string $userId, int $limit): array +{ + ensureWebImAppTables(); + $statement = $appDb->prepare( + 'select g.*, m.`role`' + . ' from `t_web_groups` g' + . ' join `t_web_group_members` m on m.`group_id` = g.`group_id` and m.`status` = 0' + . ' where g.`deleted` = 0 and m.`user_id` = ?' + . ' order by g.`updated_at_ms` desc limit ' . min(max($limit, 50), 100) + ); + $statement->execute([$userId]); + $groups = $statement->fetchAll(); + if (!$groups) { + return []; + } + + $lastMessageStatement = $appDb->prepare( + 'select * from `t_web_group_messages`' + . ' where `group_id` = ? and `recalled` = 0' + . ' order by greatest(`created_at_ms`, `edited_at_ms`) desc, `id` desc limit 1' + ); + $readStatement = $appDb->prepare( + 'select `read_at_ms` from `t_web_group_reads` where `group_id` = ? and `user_id` = ? limit 1' + ); + $lastAttachmentStatement = $appDb->prepare( + 'select `attachment_type`, `url`, `name`, `mime`, `size_bytes`' + . ' from `t_web_message_attachments` where `message_uid` = ? limit 1' + ); + $unreadStatement = $appDb->prepare( + 'select count(*) from `t_web_group_messages`' + . ' where `group_id` = ? and `sender_uid` <> ? and `recalled` = 0' + . ' and greatest(`created_at_ms`, `edited_at_ms`) > ?' + ); + $memberCountStatement = $appDb->prepare( + 'select count(*) from `t_web_group_members` where `group_id` = ? and `status` = 0' + ); + + $conversations = []; + foreach ($groups as $group) { + $groupId = (string)$group['group_id']; + $lastMessageStatement->execute([$groupId]); + $lastMessage = $lastMessageStatement->fetch(); + $readStatement->execute([$groupId, $userId]); + $readAt = (int)($readStatement->fetchColumn() ?: 0); + $unreadStatement->execute([$groupId, $userId, $readAt]); + $unread = (int)$unreadStatement->fetchColumn(); + $memberCountStatement->execute([$groupId]); + $memberCount = (int)$memberCountStatement->fetchColumn(); + + $title = (string)$group['name']; + $category = (string)$group['category']; + $isChannel = $category === 'channel'; + $lastAttachment = null; + if ($lastMessage) { + $lastAttachmentStatement->execute([(string)$lastMessage['message_uid']]); + $lastAttachmentRow = $lastAttachmentStatement->fetch(); + $lastAttachment = $lastAttachmentRow ? normalizeAttachmentRow($lastAttachmentRow) : null; + } + $subtitle = $lastMessage + ? (messagePreviewText([ + 'text' => attachmentDisplayText((string)$lastMessage['text'], $lastAttachment), + 'attachment' => $lastAttachment, + ]) ?: (string)$lastMessage['text']) + : ($isChannel ? '公告群' : ($memberCount > 0 ? $memberCount . ' 位成员' : '群组')); + $updatedAt = $lastMessage + ? max((int)$lastMessage['created_at_ms'], (int)($lastMessage['edited_at_ms'] ?? 0)) + : (int)$group['updated_at_ms']; + + $conversations[] = [ + 'id' => 'group-' . $groupId, + 'type' => $isChannel ? 'channel' : 'group', + 'target' => $groupId, + 'title' => $title, + 'subtitle' => $subtitle, + 'unread' => $unread, + 'avatarText' => mb_strtoupper(mb_substr($title, 0, 1, 'UTF-8'), 'UTF-8'), + 'updatedAt' => $updatedAt, + 'role' => (string)$group['role'], + 'memberCount' => $memberCount, + 'raw' => [ + 'type' => 'group', + 'target' => $groupId, + 'line' => 0, + 'category' => $category, + 'role' => (string)$group['role'], + ], + ]; + } + + return $conversations; +} + +function isRecallMessage(array $row): bool +{ + return (int)($row['_content_type'] ?? 0) === 80; +} + +function shouldDisplayMessageForUser(array $row, string $currentUserId): bool +{ + if (!isRecallMessage($row)) { + return true; + } + + return (string)$row['_from'] === $currentUserId; +} + +function formatMessage( + array $row, + string $currentUserId, + string $peerId, + array $receiptState = [], + array $editState = [], + ?array $attachment = null +): array +{ + $messageId = (string)$row['_mid']; + $from = (string)$row['_from']; + $contentType = (int)($row['_content_type'] ?? 0); + $isRecalled = $contentType === 80; + $text = (string)($row['_searchable_key'] ?? ''); + $timestamp = timestampMs((string)($row['_dt'] ?? ''), $messageId); + $direction = $from === $currentUserId ? 'out' : 'in'; + $messageType = $attachment ? (string)$attachment['type'] : (($contentType === 1 || $isRecalled) ? 'text' : 'unknown'); + $messageText = $text !== '' ? $text : ($isRecalled || $attachment ? '' : '[暂不支持的消息类型]'); + $messageText = attachmentDisplayText($messageText, $attachment); + $editedAt = (int)($editState['editedAt'] ?? 0); + $activityTimestamp = messageActivityTimestamp($timestamp, $editState); + $deliveryStatus = $direction === 'out' && !$isRecalled + ? outgoingDeliveryStatus($activityTimestamp, $receiptState) + : ''; + + return [ + 'id' => $messageId, + 'messageUid' => $messageId, + 'conversationId' => 'single-' . $peerId, + 'from' => $from, + 'to' => $from === $currentUserId ? $peerId : $currentUserId, + 'target' => $peerId, + 'direction' => $direction, + 'type' => $messageType, + 'contentType' => $attachment ? ((string)$attachment['type'] === 'image' ? 3 : 5) : $contentType, + 'text' => $messageText, + 'attachment' => $attachment, + 'status' => $isRecalled && $direction === 'out' ? 'recalled' : ($direction === 'out' ? $deliveryStatus : 'sent'), + 'deliveryStatus' => $deliveryStatus, + 'recalled' => $isRecalled, + 'recallStatus' => $isRecalled ? '已撤回' : '', + 'editedAt' => $editedAt, + 'timestamp' => $timestamp, + 'activityTimestamp' => $activityTimestamp, + 'displayTimestamp' => $timestamp, + ]; +} + +function loadConversationMessages(PDO $db, string $userId, string $targetId, int $limit, bool $markRead): array +{ + $table = imUserMessagesTable($userId); + $conversationKeys = [$userId . '|' . $targetId, $targetId . '|' . $userId]; + $scanLimit = min(max($limit * 4, $limit), 400); + $statement = $db->prepare( + 'select `_mid`, `_dt` from `' . $table . '`' + . ' where `_uid` = ? and `_type` = 0 and `_line` = 0' + . ' and `_target` in (?, ?)' + . ' order by `_mid` desc limit ' . $scanLimit + ); + $statement->execute([$userId, $conversationKeys[0], $conversationKeys[1]]); + $userRows = $statement->fetchAll(); + $messageRows = fetchMessageRows($db, array_column($userRows, '_mid')); + $editMap = fetchMessageEditMap($db, array_column($userRows, '_mid')); + $attachmentMap = fetchAttachmentMap(imAppDb(), array_column($userRows, '_mid')); + $receiptState = loadPeerReceiptState($db, $userId, $targetId); + + $messages = []; + $seenMessageIds = []; + $readTimestamp = 0; + foreach ($userRows as $userRow) { + $messageId = (string)$userRow['_mid']; + if (isset($seenMessageIds[$messageId]) || !isset($messageRows[$messageId])) { + continue; + } + $seenMessageIds[$messageId] = true; + if (!shouldDisplayMessageForUser($messageRows[$messageId], $userId)) { + continue; + } + $message = formatMessage( + $messageRows[$messageId], + $userId, + $targetId, + $receiptState, + $editMap[$messageId] ?? [], + $attachmentMap[$messageId] ?? null + ); + $messages[] = $message; + $readTimestamp = max($readTimestamp, (int)$message['activityTimestamp']); + if (count($messages) >= $limit) { + break; + } + } + $messages = array_reverse($messages); + + if ($markRead) { + markConversationRead($db, $userId, $targetId, $readTimestamp); + } + + return $messages; +} + +function loadRecentConversations(PDO $db, string $userId, int $limit): array +{ + $table = imUserMessagesTable($userId); + $scanLimit = min(max($limit * 20, 200), 1000); + $statement = $db->prepare( + 'select `_mid`, `_target`, `_dt` from `' . $table . '`' + . ' where `_uid` = ? and `_type` = 0 and `_line` = 0' + . ' order by `_mid` desc limit ' . $scanLimit + ); + $statement->execute([$userId]); + + $scanRows = $statement->fetchAll(); + $messageRows = fetchMessageRows($db, array_column($scanRows, '_mid')); + $editMap = fetchMessageEditMap($db, array_column($scanRows, '_mid')); + $attachmentMap = fetchAttachmentMap(imAppDb(), array_column($scanRows, '_mid')); + $latestByPeer = []; + foreach ($scanRows as $row) { + $messageId = (string)$row['_mid']; + $message = $messageRows[$messageId] ?? null; + $peerId = peerFromConversationTarget($userId, (string)$row['_target']); + if ($peerId === '' || !$message) { + continue; + } + if (!shouldDisplayMessageForUser($message, $userId)) { + continue; + } + $activityTimestamp = messageActivityTimestamp( + timestampMs((string)($message['_dt'] ?? ''), $messageId), + $editMap[$messageId] ?? [] + ); + if (!isset($latestByPeer[$peerId]) || $activityTimestamp > (int)($latestByPeer[$peerId]['_activity_at'] ?? 0)) { + $row['_activity_at'] = $activityTimestamp; + $latestByPeer[$peerId] = $row; + } + } + uasort($latestByPeer, static fn($a, $b) => (int)($b['_activity_at'] ?? 0) <=> (int)($a['_activity_at'] ?? 0)); + $latestByPeer = array_slice($latestByPeer, 0, $limit, true); + + $users = fetchUserInfoMap($db, array_keys($latestByPeer), $userId); + $readAtByPeer = loadOwnReadStateMap($db, $userId, array_keys($latestByPeer)); + $unreadByPeer = []; + $seenUnreadMessageIds = []; + + foreach ($scanRows as $row) { + $messageId = (string)$row['_mid']; + if (isset($seenUnreadMessageIds[$messageId])) { + continue; + } + $seenUnreadMessageIds[$messageId] = true; + $peerId = peerFromConversationTarget($userId, (string)$row['_target']); + $message = $messageRows[$messageId] ?? null; + if ($peerId === '' || !$message || !isset($latestByPeer[$peerId])) { + continue; + } + if ((string)$message['_from'] === $userId) { + continue; + } + if (isRecallMessage($message)) { + continue; + } + $timestamp = timestampMs((string)($message['_dt'] ?? ''), $messageId); + $activityTimestamp = messageActivityTimestamp($timestamp, $editMap[$messageId] ?? []); + if ($activityTimestamp > (int)($readAtByPeer[$peerId] ?? 0)) { + $unreadByPeer[$peerId] = (int)($unreadByPeer[$peerId] ?? 0) + 1; + } + } + + $conversations = []; + + foreach ($latestByPeer as $peerId => $userRow) { + $messageId = (string)$userRow['_mid']; + $message = isset($messageRows[$messageId]) + ? formatMessage($messageRows[$messageId], $userId, $peerId, [], $editMap[$messageId] ?? [], $attachmentMap[$messageId] ?? null) + : null; + $title = userTitle($users, $peerId); + $conversations[] = [ + 'id' => 'single-' . $peerId, + 'type' => 'single', + 'target' => $peerId, + 'title' => $title, + 'subtitle' => $message ? (messagePreviewText($message) !== '' ? messagePreviewText($message) : (empty($message['recalled']) ? '' : '已撤回')) : '', + 'unread' => (int)($unreadByPeer[$peerId] ?? 0), + 'portrait' => (string)($users[$peerId]['_portrait'] ?? ''), + 'avatarText' => mb_strtoupper(mb_substr($title, 0, 1, 'UTF-8'), 'UTF-8'), + 'updatedAt' => $message ? $message['activityTimestamp'] : timestampMs((string)($userRow['_dt'] ?? ''), $messageId), + 'raw' => ['type' => 'single', 'target' => $peerId, 'line' => 0], + ]; + } + + try { + $conversations = array_merge($conversations, loadWebGroupConversations(imAppDb(), $userId, $limit)); + } catch (Throwable) { + // Keep single-chat sync available if appdata group metadata is temporarily unavailable. + } + + usort($conversations, static fn($a, $b) => $b['updatedAt'] <=> $a['updatedAt']); + + return array_slice($conversations, 0, $limit); +} + +$payload = readJsonPayload(); +$userId = trim((string)($payload['userId'] ?? '')); +$target = trim((string)($payload['target'] ?? '')); +$limit = clampLimit($payload['limit'] ?? 50, 50, 100); +$markRead = payloadBool($payload['markRead'] ?? false); +$conversationType = normalizeConversationType($payload['conversationType'] ?? $payload['type'] ?? 0); + +if ($userId === '') { + respond(400, '缺少用户ID'); +} + +try { + $db = imDb(); + $userId = resolveUserId($db, $userId); + markUserDelivered($db, $userId, nowMs()); + + if ($target !== '') { + $targetId = resolveUserId($db, $target); + if ($conversationType === 'group' || isWebGroupTarget(imAppDb(), $target)) { + respond(0, 'success', [ + 'messages' => loadWebGroupMessages(imAppDb(), $userId, $target, $limit, $markRead), + ]); + } + respond(0, 'success', [ + 'messages' => loadConversationMessages($db, $userId, $targetId, $limit, $markRead), + ]); + } + + respond(0, 'success', [ + 'conversations' => loadRecentConversations($db, $userId, $limit), + ]); +} catch (Throwable $error) { + respond(500, $error->getMessage() ?: '消息同步失败'); +} diff --git a/dist-local/api/platform_config.php b/dist-local/api/platform_config.php new file mode 100644 index 0000000..af63018 --- /dev/null +++ b/dist-local/api/platform_config.php @@ -0,0 +1,39 @@ +prepare( + 'select `conf_name`, `conf_value`, `updated_at` from `system_conf` where `conf_name` in (?, ?)' + ); + $statement->execute(['PlatformName', 'sys_logo']); + + $config = []; + $updatedAtValues = []; + foreach ($statement->fetchAll() as $row) { + $config[(string)$row['conf_name']] = trim((string)($row['conf_value'] ?? '')); + $updatedAtValues[] = (string)($row['updated_at'] ?? ''); + } + + $platformName = $config['PlatformName'] ?? ''; + $logo = $config['sys_logo'] ?? ''; + $version = implode('|', array_values(array_filter($updatedAtValues ?? []))); + + respond(0, 'success', [ + 'platformName' => $platformName !== '' ? $platformName : $defaultName, + 'logo' => $logo !== '' ? $logo : $defaultLogo, + 'version' => $version, + ]); +} catch (Throwable $error) { + respond(500, $error->getMessage() ?: '平台配置加载失败', [ + 'platformName' => $defaultName, + 'logo' => $defaultLogo, + ]); +} diff --git a/dist-local/api/profile.php b/dist-local/api/profile.php new file mode 100644 index 0000000..cb3a75e --- /dev/null +++ b/dist-local/api/profile.php @@ -0,0 +1,321 @@ + $uid, + 'userId' => $uid, + 'name' => (string)($user['name'] ?? $user['_name'] ?? ''), + 'displayName' => (string)($user['displayName'] ?? $user['_display_name'] ?? $user['name'] ?? $user['_name'] ?? $uid), + 'mobile' => (string)($user['mobile'] ?? $user['_mobile'] ?? ''), + 'portrait' => (string)($user['portrait'] ?? $user['_portrait'] ?? ''), + 'address' => (string)($user['address'] ?? $user['_address'] ?? ''), + ]; +} + +function tryProfileUserInfo(array $payload): ?array +{ + try { + $result = requireImSuccess(imAdminPost('/admin/user/get_info', $payload), '用户资料加载失败'); + return $result ?: null; + } catch (Throwable) { + return null; + } +} + +function loadProfileUserFromDb(string $identity): ?array +{ + $statement = imDb()->prepare( + 'select `_uid`, `_name`, `_display_name`, `_mobile`, `_portrait`, `_address`' + . ' from `t_user` where `_uid` = ? or `_mobile` = ? or `_name` = ?' + . ' order by case when `_uid` = ? then 0 when `_mobile` = ? then 1 when `_name` = ? then 2 else 3 end' + . ' limit 1' + ); + $statement->execute([$identity, $identity, $identity, $identity, $identity, $identity]); + $row = $statement->fetch(); + return $row ? normalizeProfileUser($row) : null; +} + +function mergeProfileUser(array $primary, ?array $fallback): array +{ + if (!$fallback) { + return $primary; + } + + $fallback = normalizeProfileUser($fallback); + foreach (['name', 'displayName', 'mobile', 'portrait', 'address'] as $key) { + if ((string)($primary[$key] ?? '') === '' && (string)($fallback[$key] ?? '') !== '') { + $primary[$key] = (string)$fallback[$key]; + } + } + + return $primary; +} + +function persistProfileUserToDb(string $userId, string $displayName, string $portrait): void +{ + if ($portrait !== '') { + $statement = imDb()->prepare( + 'update `t_user` set `_display_name` = ?, `_portrait` = ?, `_dt` = ? where `_uid` = ? limit 1' + ); + $statement->execute([$displayName, $portrait, webImNowMs(), $userId]); + return; + } + + $statement = imDb()->prepare( + 'update `t_user` set `_display_name` = ?, `_dt` = ? where `_uid` = ? limit 1' + ); + $statement->execute([$displayName, webImNowMs(), $userId]); +} + +function loadProfileUser(string $identity, bool $preferMobile = true): array +{ + $identity = trim($identity); + if ($identity === '') { + respond(400, '缺少用户ID'); + } + + $dbUser = loadProfileUserFromDb($identity); + if ($dbUser) { + $fallback = null; + if ((string)($dbUser['portrait'] ?? '') === '') { + $fallback = tryProfileUserInfo(['userId' => $dbUser['userId']]); + if (!$fallback && (string)($dbUser['mobile'] ?? '') !== '') { + $fallback = tryProfileUserInfo(['mobile' => $dbUser['mobile']]); + } + if (!$fallback && (string)($dbUser['name'] ?? '') !== '') { + $fallback = tryProfileUserInfo(['name' => $dbUser['name']]); + } + } + + return mergeProfileUser($dbUser, $fallback); + } + + $user = null; + if ($preferMobile && preg_match('/^\+?\d{5,32}$/', $identity)) { + $user = tryProfileUserInfo(['mobile' => $identity]); + } + if (!$user) { + $user = tryProfileUserInfo(['userId' => $identity]); + } + if (!$user) { + $user = tryProfileUserInfo(['name' => $identity]); + } + if (!$user) { + respond(404, '用户资料不存在'); + } + + return normalizeProfileUser($user); +} + +function readProfileJavaSerializedString(string $data, int $offset): ?array +{ + if ($offset + 3 > strlen($data) || ord($data[$offset]) !== 0x74) { + return null; + } + + $length = (ord($data[$offset + 1]) << 8) | ord($data[$offset + 2]); + $start = $offset + 3; + if ($start + $length > strlen($data)) { + return null; + } + + return [substr($data, $start, $length), $start + $length]; +} + +function profileSessionUserId(string $sessionData): string +{ + $marker = "\x74\x00\x06userId"; + $offset = strpos($sessionData, $marker); + if ($offset === false) { + return ''; + } + + $decoded = readProfileJavaSerializedString($sessionData, $offset + strlen($marker)); + $userId = is_array($decoded) ? (string)$decoded[0] : ''; + if (!preg_match('/^[A-Za-z0-9_:@.+-]{1,128}$/', $userId)) { + return ''; + } + + return $userId; +} + +function requireProfileUpdateAuth(string $targetUserId): void +{ + $authToken = trim((string)($_SERVER['HTTP_AUTHTOKEN'] ?? $_SERVER['HTTP_AUTH_TOKEN'] ?? '')); + if ($authToken === '') { + respond(401, '请重新登录后再修改资料'); + } + + $statement = imAppDb()->prepare('select `session_data` from `shiro_session` where `session_id` = ? limit 1'); + $statement->execute([$authToken]); + $row = $statement->fetch(); + if (!$row) { + respond(401, '登录状态已失效,请重新登录'); + } + + $sessionUserId = profileSessionUserId((string)$row['session_data']); + if ($sessionUserId === '' || $sessionUserId !== $targetUserId) { + respond(403, '只能修改自己的资料'); + } +} + +function profileUploadedFileExtension(string $fileName, string $mime): string +{ + $extension = strtolower((string)pathinfo($fileName, PATHINFO_EXTENSION)); + $extension = preg_replace('/[^a-z0-9]/', '', $extension) ?: ''; + if ($extension === 'jpeg') { + $extension = 'jpg'; + } + + $allowedExtensions = ['jpg', 'png', 'gif', 'webp', 'bmp', 'ico']; + if (in_array($extension, $allowedExtensions, true)) { + return $extension; + } + + return match ($mime) { + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif', + 'image/webp' => 'webp', + 'image/bmp', 'image/x-ms-bmp' => 'bmp', + 'image/x-icon', 'image/vnd.microsoft.icon' => 'ico', + default => 'jpg', + }; +} + +function profilePublicMediaBaseUrl(): string +{ + $isHttps = (string)($_SERVER['HTTPS'] ?? '') !== '' && strtolower((string)$_SERVER['HTTPS']) !== 'off'; + $scheme = $isHttps ? 'https' : 'http'; + $host = (string)($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '127.0.0.1'); + $host = trim(preg_replace('/:\d+$/', '', $host) ?: '127.0.0.1'); + + return $scheme . '://' . $host . ':' . IM_LOCAL_MEDIA_PUBLIC_PORT; +} + +function uploadProfilePortrait(array $file): string +{ + if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { + throw new RuntimeException('头像上传失败'); + } + if (($file['size'] ?? 0) <= 0 || ($file['size'] ?? 0) > 5 * 1024 * 1024) { + throw new RuntimeException('头像不能超过5MB'); + } + + $tmpName = (string)($file['tmp_name'] ?? ''); + $mime = mime_content_type($tmpName) ?: ''; + if (!str_starts_with($mime, 'image/')) { + throw new RuntimeException('头像必须是图片文件'); + } + + if (!is_dir(IM_LOCAL_MEDIA_ROOT) || !is_writable(IM_LOCAL_MEDIA_ROOT)) { + throw new RuntimeException('头像存储目录不可写'); + } + + $datePath = 'fs/' . PROFILE_PORTRAIT_MEDIA_TYPE . '/' . date('Y/m/d/H/i/s'); + $targetDir = rtrim(IM_LOCAL_MEDIA_ROOT, '/') . '/' . $datePath; + if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true)) { + throw new RuntimeException('头像存储目录创建失败'); + } + + $extension = profileUploadedFileExtension((string)($file['name'] ?? 'avatar.jpg'), $mime); + $targetName = bin2hex(random_bytes(12)) . '.' . $extension; + $targetPath = $targetDir . '/' . $targetName; + if (!move_uploaded_file($tmpName, $targetPath)) { + throw new RuntimeException('头像文件保存失败'); + } + @chmod($targetPath, 0644); + + return profilePublicMediaBaseUrl() . '/' . $datePath . '/' . $targetName; +} + +try { + $contentType = (string)($_SERVER['CONTENT_TYPE'] ?? ''); + $isMultipart = stripos($contentType, 'multipart/form-data') !== false; + $payload = $isMultipart ? [] : readJsonPayload(); + $action = $isMultipart + ? profileRequestValue('action', 'update') + : trim((string)($payload['action'] ?? 'get')); + $userId = $isMultipart + ? profileRequestValue('userId') + : trim((string)($payload['userId'] ?? $_GET['userId'] ?? '')); + + $currentProfile = loadProfileUser($userId); + + if ($action === 'get') { + respond(0, 'success', ['profile' => $currentProfile]); + } + + if ($action !== 'update') { + respond(400, '不支持的资料操作'); + } + requireProfileUpdateAuth((string)$currentProfile['userId']); + + $displayName = $isMultipart + ? profileRequestValue('displayName') + : trim((string)($payload['displayName'] ?? '')); + if ($displayName === '') { + respond(400, '昵称不能为空'); + } + if (mb_strlen($displayName, 'UTF-8') > 64) { + respond(400, '昵称不能超过64个字'); + } + + $portrait = ''; + if (!empty($_FILES['portraitFile']) && ($_FILES['portraitFile']['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_NO_FILE) { + $portrait = uploadProfilePortrait($_FILES['portraitFile']); + } + + $flag = PROFILE_DISPLAY_NAME_FLAG; + $userInfo = [ + 'userId' => $currentProfile['userId'], + 'displayName' => $displayName, + ]; + if ($portrait !== '') { + $flag |= PROFILE_PORTRAIT_FLAG; + $userInfo['portrait'] = $portrait; + } + + requireImSuccess( + imAdminPost('/admin/user/update', [ + 'flag' => $flag, + 'userInfo' => $userInfo, + ]), + '用户资料保存失败' + ); + persistProfileUserToDb($currentProfile['userId'], $displayName, $portrait); + + $updatedProfile = loadProfileUser($currentProfile['userId'], false); + if ($portrait === '' && (string)$updatedProfile['portrait'] === '' && (string)($currentProfile['portrait'] ?? '') !== '') { + $updatedProfile['portrait'] = (string)$currentProfile['portrait']; + } + if ($portrait !== '' && (string)$updatedProfile['portrait'] === '') { + $updatedProfile['portrait'] = $portrait; + } + if ((string)$updatedProfile['displayName'] === '') { + $updatedProfile['displayName'] = $displayName; + } + + respond(0, 'success', ['profile' => $updatedProfile]); +} catch (Throwable $error) { + respond(500, $error->getMessage() ?: '用户资料服务暂不可用'); +} diff --git a/dist-local/api/register_config.php b/dist-local/api/register_config.php new file mode 100644 index 0000000..887de79 --- /dev/null +++ b/dist-local/api/register_config.php @@ -0,0 +1,18 @@ + 0, + 'message' => 'success', + 'data' => [ + 'methods' => [ + 'phone' => true, + 'email' => false, + 'username' => false, + ], + 'mobile_real_verify' => false, + ], +], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); diff --git a/dist-local/api/send_message.php b/dist-local/api/send_message.php new file mode 100644 index 0000000..c06af18 --- /dev/null +++ b/dist-local/api/send_message.php @@ -0,0 +1,411 @@ +setTimezone(new DateTimeZone(IM_DB_TIMEZONE)); + $currentMinutes = ((int)$now->format('H')) * 60 + (int)$now->format('i'); + [$startHour, $startMinute] = array_map('intval', explode(':', $dailyStart)); + [$endHour, $endMinute] = array_map('intval', explode(':', $dailyEnd)); + $startMinutes = $startHour * 60 + $startMinute; + $endMinutes = $endHour * 60 + $endMinute; + if ($startMinutes === $endMinutes) { + return true; + } + if ($startMinutes < $endMinutes) { + return $currentMinutes >= $startMinutes && $currentMinutes < $endMinutes; + } + + return $currentMinutes >= $startMinutes || $currentMinutes < $endMinutes; +} + +function isGroupMutedForSender(PDO $appDb, string $groupId, string $role, int $nowMs): bool +{ + if ($role === 'owner') { + return false; + } + + $statement = $appDb->prepare('select * from `t_web_group_mute_rules` where `group_id` = ? and `enabled` = 1 limit 1'); + $statement->execute([$groupId]); + $rule = $statement->fetch(); + if (!is_array($rule)) { + return false; + } + + $mode = (string)$rule['mode']; + if ($mode === 'off') { + return false; + } + if ($mode === 'all_except_admins' && $role === 'admin') { + return false; + } + if (!in_array($mode, ['all_include_admins', 'all_except_admins'], true)) { + return false; + } + + $scheduleType = (string)$rule['schedule_type']; + if ($scheduleType === 'manual') { + return true; + } + if ($scheduleType === 'daily') { + return isDailyMuteActive((string)$rule['daily_start'], (string)$rule['daily_end'], $nowMs); + } + + $startsAtMs = (int)$rule['starts_at_ms']; + $endsAtMs = (int)$rule['ends_at_ms']; + return $startsAtMs > 0 && $endsAtMs > $startsAtMs && $nowMs >= $startsAtMs && $nowMs < $endsAtMs; +} + +function messageRequestValue(array $payload, string $key, string $default = ''): string +{ + return trim((string)($_POST[$key] ?? $payload[$key] ?? $default)); +} + +function safeUploadName(string $fileName): string +{ + $name = basename(str_replace('\\', '/', $fileName)); + $name = preg_replace('/[^\p{L}\p{N}._ -]+/u', '_', $name) ?: 'file'; + $name = trim($name, " .\t\n\r\0\x0B"); + + return $name !== '' ? mb_substr($name, 0, 180, 'UTF-8') : 'file'; +} + +function uploadExtension(string $fileName, string $mime, string $attachmentType): string +{ + $extension = strtolower((string)pathinfo($fileName, PATHINFO_EXTENSION)); + $extension = preg_replace('/[^a-z0-9]/', '', $extension) ?: ''; + if ($extension === 'jpeg') { + $extension = 'jpg'; + } + + if ($attachmentType === 'image') { + if (in_array($extension, ['jpg', 'png', 'gif', 'webp', 'bmp', 'svg', 'avif', 'heic', 'heif'], true)) { + return $extension; + } + return match ($mime) { + 'image/jpeg' => 'jpg', + 'image/png' => 'png', + 'image/gif' => 'gif', + 'image/webp' => 'webp', + 'image/bmp', 'image/x-ms-bmp' => 'bmp', + 'image/svg+xml' => 'svg', + 'image/avif' => 'avif', + 'image/heic' => 'heic', + 'image/heif' => 'heif', + default => 'jpg', + }; + } + + if ($extension !== '') { + return $extension; + } + + return 'bin'; +} + +function detectedUploadMime(string $tmpName): string +{ + $mime = mime_content_type($tmpName) ?: ''; + if ($mime !== '' && $mime !== 'application/octet-stream') { + return $mime; + } + + $imageInfo = @getimagesize($tmpName); + if (is_array($imageInfo) && !empty($imageInfo['mime'])) { + return (string)$imageInfo['mime']; + } + + return $mime !== '' ? $mime : 'application/octet-stream'; +} + +function normalizedImageUploadMime(string $tmpName, string $mime): string +{ + if (str_starts_with($mime, 'image/')) { + return $mime; + } + + $imageInfo = @getimagesize($tmpName); + $imageMime = is_array($imageInfo) ? (string)($imageInfo['mime'] ?? '') : ''; + if ($imageMime !== '' && str_starts_with($imageMime, 'image/')) { + return $imageMime; + } + + throw new RuntimeException('请选择图片文件'); +} + +function mediaPublicBaseUrl(): string +{ + $isHttps = (string)($_SERVER['HTTPS'] ?? '') !== '' && strtolower((string)$_SERVER['HTTPS']) !== 'off'; + $scheme = $isHttps ? 'https' : 'http'; + $host = trim(preg_replace('/:\d+$/', '', (string)($_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'] ?? '127.0.0.1')) ?: '127.0.0.1'); + + return $scheme . '://' . $host . ':' . WEB_MEDIA_PUBLIC_PORT; +} + +function saveMessageAttachmentFile(array $file, string $attachmentType): array +{ + if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK) { + throw new RuntimeException('文件上传失败'); + } + $size = (int)($file['size'] ?? 0); + $maxSize = $attachmentType === 'image' ? WEB_IMAGE_MAX_BYTES : WEB_FILE_MAX_BYTES; + if ($size <= 0 || $size > $maxSize) { + throw new RuntimeException($attachmentType === 'image' ? '图片不能超过10MB' : '文件不能超过50MB'); + } + if (!is_dir(WEB_MEDIA_ROOT) || !is_writable(WEB_MEDIA_ROOT)) { + throw new RuntimeException('媒体存储目录不可写'); + } + + $tmpName = (string)($file['tmp_name'] ?? ''); + if ($tmpName === '' || !is_uploaded_file($tmpName)) { + throw new RuntimeException('文件上传失败'); + } + $mime = detectedUploadMime($tmpName); + if ($attachmentType === 'image') { + $mime = normalizedImageUploadMime($tmpName, $mime); + } + + $mediaType = $attachmentType === 'image' ? WEB_MEDIA_TYPE_IMAGE : WEB_MEDIA_TYPE_FILE; + $datePath = 'fs/' . $mediaType . '/' . date('Y/m/d/H/i/s'); + $targetDir = rtrim(WEB_MEDIA_ROOT, '/') . '/' . $datePath; + if (!is_dir($targetDir) && !mkdir($targetDir, 0775, true)) { + throw new RuntimeException('媒体存储目录创建失败'); + } + + $originalName = safeUploadName((string)($file['name'] ?? ($attachmentType === 'image' ? 'image.jpg' : 'file'))); + $extension = uploadExtension($originalName, $mime, $attachmentType); + $targetName = bin2hex(random_bytes(12)) . '.' . $extension; + $targetPath = $targetDir . '/' . $targetName; + if (!move_uploaded_file($tmpName, $targetPath)) { + throw new RuntimeException('文件保存失败'); + } + @chmod($targetPath, 0644); + + return [ + 'type' => $attachmentType, + 'url' => mediaPublicBaseUrl() . '/' . $datePath . '/' . $targetName, + 'name' => $originalName, + 'mime' => $mime, + 'size' => $size, + 'mediaType' => $mediaType, + ]; +} + +function storeMessageAttachment( + PDO $appDb, + string $messageUid, + string $conversationType, + string $target, + string $sender, + array $attachment, + int $timestamp +): void { + if ($messageUid === '' || empty($attachment['url'])) { + return; + } + + $statement = $appDb->prepare( + 'insert into `t_web_message_attachments`' + . ' (`message_uid`, `conversation_type`, `target_id`, `sender_uid`, `attachment_type`, `url`, `name`, `mime`, `size_bytes`, `created_at_ms`)' + . ' values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' + . ' on duplicate key update' + . ' `attachment_type` = values(`attachment_type`),' + . ' `url` = values(`url`),' + . ' `name` = values(`name`),' + . ' `mime` = values(`mime`),' + . ' `size_bytes` = values(`size_bytes`)' + ); + $statement->execute([ + $messageUid, + $conversationType, + $target, + $sender, + (string)$attachment['type'], + (string)$attachment['url'], + (string)$attachment['name'], + (string)$attachment['mime'], + (int)$attachment['size'], + $timestamp, + ]); +} + +function attachmentFromRequest(string $messageType): ?array +{ + if (!in_array($messageType, ['image', 'file'], true)) { + return null; + } + + $attachmentFile = $_FILES['attachment'] ?? $_FILES['file'] ?? null; + if (!is_array($attachmentFile)) { + respond(400, '缺少上传文件'); + } + + return saveMessageAttachmentFile($attachmentFile, $messageType); +} + +$contentTypeHeader = (string)($_SERVER['CONTENT_TYPE'] ?? ''); +$isMultipart = stripos($contentTypeHeader, 'multipart/form-data') !== false; +$payload = $isMultipart ? [] : readJsonPayload(); +$sender = messageRequestValue($payload, 'sender'); +$target = messageRequestValue($payload, 'target'); +$text = messageRequestValue($payload, 'text'); +$messageType = messageRequestValue($payload, 'messageType', 'text'); +$conversationType = $_POST['conversationType'] ?? $_POST['type'] ?? $payload['conversationType'] ?? $payload['type'] ?? 0; +$isGroupConversation = in_array((string)$conversationType, ['1', 'group', 'channel'], true); +$attachment = null; + +if ($sender === '' || $target === '' || ($text === '' && !in_array($messageType, ['image', 'file'], true))) { + respond(400, '缺少发送者、接收者或消息内容'); +} + +try { + if ($isGroupConversation) { + ensureWebImAppTables(); + $appDb = imAppDb(); + $memberStatement = $appDb->prepare( + 'select g.`group_id`, g.`category`, g.`owner_uid`, m.`role`' + . ' from `t_web_groups` g' + . ' join `t_web_group_members` m on m.`group_id` = g.`group_id` and m.`status` = 0' + . ' where g.`deleted` = 0 and g.`group_id` = ? and m.`user_id` = ?' + . ' limit 1' + ); + $memberStatement->execute([$target, $sender]); + $group = $memberStatement->fetch(); + if (!$group) { + respond(403, '你不在该群组中'); + } + $category = (string)($group['category'] ?? 'group'); + $role = (string)($group['role'] ?? 'member'); + if ($category === 'channel' && !in_array($role, ['owner', 'admin'], true)) { + respond(403, '公告群仅管理员可发言'); + } + + $now = webImNowMs(); + if ($category !== 'channel' && isGroupMutedForSender($appDb, $target, $role, $now)) { + respond(403, '当前群组处于禁言时段'); + } + $attachment = attachmentFromRequest($messageType); + $messageUid = webImMessageUid(); + $insert = $appDb->prepare( + 'insert into `t_web_group_messages`' + . ' (`message_uid`, `group_id`, `sender_uid`, `text`, `created_at_ms`, `edited_at_ms`, `recalled`)' + . ' values (?, ?, ?, ?, ?, 0, 0)' + ); + $insert->execute([$messageUid, $target, $sender, $text, $now]); + if ($attachment) { + storeMessageAttachment($appDb, $messageUid, 'group', $target, $sender, $attachment, $now); + } + $updateGroup = $appDb->prepare('update `t_web_groups` set `updated_at_ms` = ? where `group_id` = ?'); + $updateGroup->execute([$now, $target]); + + $nativeError = ''; + try { + $pushContent = $text !== '' + ? $text + : ($attachment + ? ((string)$attachment['type'] === 'image' ? '[图片]' : '[文件] ' . (string)$attachment['name']) + : $text); + $payloadBody = [ + 'type' => $attachment ? ((string)$attachment['type'] === 'image' ? 3 : 5) : 1, + 'searchableContent' => $pushContent, + 'pushContent' => $pushContent, + 'content' => $text, + 'persistFlag' => 3, + ]; + if ($attachment) { + $payloadBody['mediaType'] = (int)$attachment['mediaType']; + $payloadBody['remoteMediaUrl'] = (string)$attachment['url']; + $payloadBody['extra'] = json_encode([ + 'name' => (string)$attachment['name'], + 'mime' => (string)$attachment['mime'], + 'size' => (int)$attachment['size'], + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + requireImSuccess(imAdminPost('/admin/message/send', [ + 'sender' => $sender, + 'conv' => [ + 'type' => 1, + 'target' => $target, + 'line' => 0, + ], + 'payload' => $payloadBody, + 'userMessage' => true, + ]), '原生群消息发送失败'); + } catch (Throwable $error) { + $nativeError = $error->getMessage() ?: '原生群消息发送失败'; + } + + respond(0, 'success', [ + 'messageUid' => $messageUid, + 'timestamp' => $now, + 'senderRole' => $role, + 'attachment' => $attachment, + 'nativeError' => $nativeError, + ]); + } + + $attachment = attachmentFromRequest($messageType); + $pushContent = $text !== '' + ? $text + : ($attachment + ? ((string)$attachment['type'] === 'image' ? '[图片]' : '[文件] ' . (string)$attachment['name']) + : $text); + $payloadBody = [ + 'type' => $attachment ? ((string)$attachment['type'] === 'image' ? 3 : 5) : 1, + 'searchableContent' => $pushContent, + 'pushContent' => $pushContent, + 'content' => $text, + 'persistFlag' => 3, + ]; + if ($attachment) { + $payloadBody['mediaType'] = (int)$attachment['mediaType']; + $payloadBody['remoteMediaUrl'] = (string)$attachment['url']; + $payloadBody['extra'] = json_encode([ + 'name' => (string)$attachment['name'], + 'mime' => (string)$attachment['mime'], + 'size' => (int)$attachment['size'], + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + + $result = requireImSuccess(imAdminPost('/admin/message/send', [ + 'sender' => $sender, + 'conv' => [ + 'type' => 0, + 'target' => $target, + 'line' => 0, + ], + 'payload' => $payloadBody, + 'userMessage' => true, + ]), '发送失败'); + + $messageUid = isset($result['messageUid']) ? (string)$result['messageUid'] : ''; + $timestamp = (int)($result['timestamp'] ?? webImNowMs()); + if ($attachment && $messageUid !== '') { + ensureWebImAppTables(); + storeMessageAttachment(imAppDb(), $messageUid, 'single', $target, $sender, $attachment, $timestamp); + } + + respond(0, 'success', [ + 'messageUid' => $messageUid !== '' ? $messageUid : null, + 'timestamp' => $timestamp, + 'attachment' => $attachment, + ]); +} catch (Throwable $error) { + respond(500, $error->getMessage() ?: '发送失败'); +} diff --git a/dist-local/api/update_alias.php b/dist-local/api/update_alias.php new file mode 100644 index 0000000..7f3be6c --- /dev/null +++ b/dist-local/api/update_alias.php @@ -0,0 +1,61 @@ + 32) { + respond(400, '备注名不能超过32个字'); +} + +try { + ensureWebImAppTables(); + $imDb = imDb(); + $exists = $imDb->prepare('select 1 from `t_user` where `_uid` = ? limit 1'); + $exists->execute([$targetId]); + if (!$exists->fetchColumn()) { + respond(404, '没有找到该用户'); + } + + $friend = $imDb->prepare('select 1 from `t_friend` where `_uid` = ? and `_friend_uid` = ? limit 1'); + $friend->execute([$userId, $targetId]); + $isContact = (bool)$friend->fetchColumn(); + + if ($isContact) { + requireImSuccess( + imAdminPost('/admin/friend/set_alias', [ + 'operator' => $userId, + 'targetId' => $targetId, + 'alias' => $alias, + ]), + '备注名修改失败' + ); + } + saveWebUserRemark(imAppDb(), $userId, $targetId, $alias); + + respond(0, 'success', [ + 'contact' => [ + 'uid' => $targetId, + 'alias' => $alias, + 'isContact' => $isContact, + ], + ]); +} catch (Throwable $error) { + respond(500, $error->getMessage() ?: '备注名服务暂不可用'); +} diff --git a/dist-local/config/runtime-config.js b/dist-local/config/runtime-config.js new file mode 100644 index 0000000..7b23bd7 --- /dev/null +++ b/dist-local/config/runtime-config.js @@ -0,0 +1,13 @@ +window.JC_WEB_IM_CONFIG = { + adminServer: 'http://18.141.44.165:8100', + appServer: '/app-server', + qrAppServer: 'http://18.141.44.165:8888', + platformConfigApi: '/api/platform_config.php', + registerConfigApi: 'http://18.141.44.165:8100/api/register-config', + imServerHost: '18.141.44.165', + imRoutePort: 80, + useWss: false, + wsPath: '/ws', + sdkMode: 'legacy-http', + syncIntervalMs: 2500 +}; diff --git a/dist-local/config/runtime-config.local.js b/dist-local/config/runtime-config.local.js new file mode 100644 index 0000000..ff44ed1 --- /dev/null +++ b/dist-local/config/runtime-config.local.js @@ -0,0 +1,18 @@ +window.JC_WEB_IM_CONFIG = { + runtimeEnvironment: 'local', + adminServer: 'http://18.141.44.165:8100', + appServer: '/app-server', + qrAppServer: 'http://18.141.44.165:8888', + platformConfigApi: '/local-api/platform_config.php', + registerConfigApi: '/local-api/register_config.php', + registerPhoneExistsApi: 'http://18.141.44.165:8100/api/register-phone-exists', + imServerHost: '18.141.44.165', + imRoutePort: 80, + useWss: false, + wsPath: '/ws', + sdkMode: 'legacy-http', + syncIntervalMs: 2500, + sseEnabled: false, + sseTicketApi: '/local-api/sse_ticket.php', + sseUrl: '/ws-events' +}; diff --git a/dist-local/config/runtime-config.production.js b/dist-local/config/runtime-config.production.js new file mode 100644 index 0000000..86d2aed --- /dev/null +++ b/dist-local/config/runtime-config.production.js @@ -0,0 +1,23 @@ +(() => { + const protocol = window.location.protocol || 'http:'; + const host = window.location.hostname || '18.141.44.165'; + + window.JC_WEB_IM_CONFIG = { + runtimeEnvironment: 'production', + adminServer: `${protocol}//${host}:8100`, + appServer: `${protocol}//${host}:8888`, + qrAppServer: `${protocol}//${host}:8888`, + platformConfigApi: '/api/platform_config.php', + registerConfigApi: `${protocol}//${host}:8100/api/register-config`, + registerPhoneExistsApi: `${protocol}//${host}:8100/api/register-phone-exists`, + imServerHost: host, + imRoutePort: 80, + useWss: false, + wsPath: '/ws', + sdkMode: 'legacy-http', + syncIntervalMs: 15000, + sseEnabled: true, + sseTicketApi: '/api/sse_ticket.php', + sseUrl: '/ws-events' + }; +})(); diff --git a/dist-local/favicon.ico b/dist-local/favicon.ico new file mode 100644 index 0000000..a758e41 Binary files /dev/null and b/dist-local/favicon.ico differ diff --git a/dist-local/img/logo.png b/dist-local/img/logo.png new file mode 100644 index 0000000..1ad08f9 Binary files /dev/null and b/dist-local/img/logo.png differ diff --git a/dist-local/index.html b/dist-local/index.html new file mode 100644 index 0000000..0656b08 --- /dev/null +++ b/dist-local/index.html @@ -0,0 +1,14 @@ + + + + + + Web IM + + + + + +
+ + diff --git a/dist-local/js/app.js b/dist-local/js/app.js new file mode 100644 index 0000000..4c06e9b --- /dev/null +++ b/dist-local/js/app.js @@ -0,0 +1,1045 @@ +/* + * ATTENTION: The "eval" devtool has been used (maybe by default in mode: "development"). + * This devtool is neither made for production nor for readable output files. + * It uses "eval()" calls to create a separate source file in the browser devtools. + * If you are trying to read the output file, select a different devtool (https://webpack.js.org/configuration/devtool/) + * or disable the default devtool with "devtool: false". + * If you are looking for production-ready output files, see mode: "production" (https://webpack.js.org/configuration/mode/). + */ +/******/ (function() { // webpackBootstrap +/******/ var __webpack_modules__ = ({ + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/App.vue?vue&type=script&setup=true&lang=js": +/*!*************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/App.vue?vue&type=script&setup=true&lang=js ***! + \*************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n/* harmony import */ var _components_LoginView_vue__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./components/LoginView.vue */ \"./src/components/LoginView.vue\");\n/* harmony import */ var _components_ChatShell_vue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./components/ChatShell.vue */ \"./src/components/ChatShell.vue\");\n/* harmony import */ var _stores_session__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./stores/session */ \"./src/stores/session.js\");\n\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n __name: 'App',\n setup(__props, {\n expose: __expose\n }) {\n __expose();\n const session = (0,_stores_session__WEBPACK_IMPORTED_MODULE_3__.useSessionStore)();\n (0,vue__WEBPACK_IMPORTED_MODULE_0__.onMounted)(() => {\n session.restore();\n });\n const __returned__ = {\n session,\n onMounted: vue__WEBPACK_IMPORTED_MODULE_0__.onMounted,\n LoginView: _components_LoginView_vue__WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n ChatShell: _components_ChatShell_vue__WEBPACK_IMPORTED_MODULE_2__[\"default\"],\n get useSessionStore() {\n return _stores_session__WEBPACK_IMPORTED_MODULE_3__.useSessionStore;\n }\n };\n Object.defineProperty(__returned__, '__isScriptSetup', {\n enumerable: false,\n value: true\n });\n return __returned__;\n }\n});\n\n//# sourceURL=webpack://jc-web-im/./src/App.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/ChatShell.vue?vue&type=script&setup=true&lang=js": +/*!******************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/ChatShell.vue?vue&type=script&setup=true&lang=js ***! + \******************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n/* harmony import */ var _sidebar_NavigationRail_vue__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./sidebar/NavigationRail.vue */ \"./src/components/sidebar/NavigationRail.vue\");\n/* harmony import */ var _sidebar_ConversationList_vue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./sidebar/ConversationList.vue */ \"./src/components/sidebar/ConversationList.vue\");\n/* harmony import */ var _sidebar_ContactList_vue__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./sidebar/ContactList.vue */ \"./src/components/sidebar/ContactList.vue\");\n/* harmony import */ var _chat_ChatPanel_vue__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./chat/ChatPanel.vue */ \"./src/components/chat/ChatPanel.vue\");\n/* harmony import */ var _contacts_ContactPanel_vue__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./contacts/ContactPanel.vue */ \"./src/components/contacts/ContactPanel.vue\");\n/* harmony import */ var _favorites_FavoriteList_vue__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./favorites/FavoriteList.vue */ \"./src/components/favorites/FavoriteList.vue\");\n/* harmony import */ var _favorites_FavoritePanel_vue__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./favorites/FavoritePanel.vue */ \"./src/components/favorites/FavoritePanel.vue\");\n/* harmony import */ var _stores_conversations__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! @/stores/conversations */ \"./src/stores/conversations.js\");\n/* harmony import */ var _stores_navigation__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! @/stores/navigation */ \"./src/stores/navigation.js\");\n/* harmony import */ var _stores_contacts__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! @/stores/contacts */ \"./src/stores/contacts.js\");\n\n\n\n\n\n\n\n\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n __name: 'ChatShell',\n setup(__props, {\n expose: __expose\n }) {\n __expose();\n const conversations = (0,_stores_conversations__WEBPACK_IMPORTED_MODULE_8__.useConversationStore)();\n const navigation = (0,_stores_navigation__WEBPACK_IMPORTED_MODULE_9__.useNavigationStore)();\n const contacts = (0,_stores_contacts__WEBPACK_IMPORTED_MODULE_10__.useContactStore)();\n (0,vue__WEBPACK_IMPORTED_MODULE_0__.onMounted)(() => {\n conversations.refreshRemoteConversations();\n contacts.refreshContacts();\n });\n const __returned__ = {\n conversations,\n navigation,\n contacts,\n onMounted: vue__WEBPACK_IMPORTED_MODULE_0__.onMounted,\n NavigationRail: _sidebar_NavigationRail_vue__WEBPACK_IMPORTED_MODULE_1__[\"default\"],\n ConversationList: _sidebar_ConversationList_vue__WEBPACK_IMPORTED_MODULE_2__[\"default\"],\n ContactList: _sidebar_ContactList_vue__WEBPACK_IMPORTED_MODULE_3__[\"default\"],\n ChatPanel: _chat_ChatPanel_vue__WEBPACK_IMPORTED_MODULE_4__[\"default\"],\n ContactPanel: _contacts_ContactPanel_vue__WEBPACK_IMPORTED_MODULE_5__[\"default\"],\n FavoriteList: _favorites_FavoriteList_vue__WEBPACK_IMPORTED_MODULE_6__[\"default\"],\n FavoritePanel: _favorites_FavoritePanel_vue__WEBPACK_IMPORTED_MODULE_7__[\"default\"],\n get useConversationStore() {\n return _stores_conversations__WEBPACK_IMPORTED_MODULE_8__.useConversationStore;\n },\n get useNavigationStore() {\n return _stores_navigation__WEBPACK_IMPORTED_MODULE_9__.useNavigationStore;\n },\n get useContactStore() {\n return _stores_contacts__WEBPACK_IMPORTED_MODULE_10__.useContactStore;\n }\n };\n Object.defineProperty(__returned__, '__isScriptSetup', {\n enumerable: false,\n value: true\n });\n return __returned__;\n }\n});\n\n//# sourceURL=webpack://jc-web-im/./src/components/ChatShell.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/LoginView.vue?vue&type=script&setup=true&lang=js": +/*!******************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/LoginView.vue?vue&type=script&setup=true&lang=js ***! + \******************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.includes.js */ \"./node_modules/core-js/modules/es.array.includes.js\");\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var axios__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! axios */ \"./node_modules/axios/lib/axios.js\");\n/* harmony import */ var jr_qrcode__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! jr-qrcode */ \"./node_modules/jr-qrcode/dist/jr-qrcode.js\");\n/* harmony import */ var jr_qrcode__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(jr_qrcode__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @/config */ \"./src/config.js\");\n/* harmony import */ var _stores_session__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @/stores/session */ \"./src/stores/session.js\");\n/* harmony import */ var _services_imClient__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! @/services/imClient */ \"./src/services/imClient.js\");\n\n\n\n\n\n\n\nconst QR_CODE_PREFIX_PC_SESSION = 'wildfirechat://pcsession/';\nconst defaultPlatformLogo = '/favicon.ico';\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n __name: 'LoginView',\n setup(__props, {\n expose: __expose\n }) {\n __expose();\n const session = (0,_stores_session__WEBPACK_IMPORTED_MODULE_5__.useSessionStore)();\n const view = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)('login');\n const loginMode = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)('phone');\n const loginAccount = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)('');\n const loginPassword = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)('');\n const registerType = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)(1);\n const registerForm = (0,vue__WEBPACK_IMPORTED_MODULE_3__.reactive)({\n number: '',\n code: '',\n password: '',\n confirmPassword: '',\n invitationCode: ''\n });\n const sendingCode = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)(false);\n const info = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)('');\n const qrStatus = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)('idle');\n const qrCodeImage = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)('');\n const qrSessionToken = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)('');\n const qrUserName = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)('');\n const qrUserPortrait = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)('');\n let qrGeneration = 0;\n let qrExpireTimer = null;\n const defaultPlatformName = _config__WEBPACK_IMPORTED_MODULE_4__.appConfig.appName;\n const platformName = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)(defaultPlatformName);\n const platformLogo = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)(defaultPlatformLogo);\n const canSubmitLogin = (0,vue__WEBPACK_IMPORTED_MODULE_3__.computed)(() => loginAccount.value.length >= 3 && loginPassword.value.length >= 1);\n const canRequestEmailCode = (0,vue__WEBPACK_IMPORTED_MODULE_3__.computed)(() => registerType.value === 2 && /\\S+@\\S+\\.\\S+/.test(registerForm.number));\n const canSubmitRegister = (0,vue__WEBPACK_IMPORTED_MODULE_3__.computed)(() => {\n if (registerForm.password.length < 1 || registerForm.password !== registerForm.confirmPassword) {\n return false;\n }\n if (registerType.value === 2) {\n return canRequestEmailCode.value && registerForm.code.length >= 1;\n }\n return registerForm.number.length >= 5;\n });\n const qrUserInitial = (0,vue__WEBPACK_IMPORTED_MODULE_3__.computed)(() => String(qrUserName.value || 'U').slice(0, 1).toUpperCase());\n const qrStatusTitle = (0,vue__WEBPACK_IMPORTED_MODULE_3__.computed)(() => {\n if (session.connecting || qrStatus.value === 'confirmed') {\n return '正在进入';\n }\n if (qrStatus.value === 'loading') {\n return '正在生成二维码';\n }\n if (qrStatus.value === 'scanned') {\n return qrUserName.value ? `${qrUserName.value} 已扫码` : '已扫码,待确认';\n }\n if (qrStatus.value === 'expired') {\n return '二维码已过期';\n }\n if (qrStatus.value === 'error') {\n return '二维码加载失败';\n }\n return '扫码登录';\n });\n const qrStatusText = (0,vue__WEBPACK_IMPORTED_MODULE_3__.computed)(() => {\n if (session.connecting || qrStatus.value === 'confirmed') {\n return '登录信息已确认,正在建立连接';\n }\n if (qrStatus.value === 'loading') {\n return '正在创建扫码登录会话';\n }\n if (qrStatus.value === 'scanned') {\n return '请在手机 App 上确认登录';\n }\n if (qrStatus.value === 'expired') {\n return '请刷新二维码后重新扫码';\n }\n if (qrStatus.value === 'error') {\n return '请稍后重试';\n }\n return '使用已登录的手机 App 扫描二维码';\n });\n const qrRefreshText = (0,vue__WEBPACK_IMPORTED_MODULE_3__.computed)(() => {\n if (qrStatus.value === 'loading') {\n return '生成中...';\n }\n if (qrStatus.value === 'expired' || qrStatus.value === 'error') {\n return '重新生成二维码';\n }\n return '刷新二维码';\n });\n (0,vue__WEBPACK_IMPORTED_MODULE_3__.onMounted)(() => {\n loadPlatformConfig();\n });\n (0,vue__WEBPACK_IMPORTED_MODULE_3__.onBeforeUnmount)(() => {\n stopQrLogin();\n });\n function clearFormMessage() {\n session.error = '';\n info.value = '';\n }\n function setLoginMode(mode) {\n loginMode.value = mode;\n loginAccount.value = '';\n loginPassword.value = '';\n clearFormMessage();\n if (mode === 'qr') {\n startQrLogin();\n return;\n }\n stopQrLogin();\n }\n async function submitLogin() {\n clearFormMessage();\n const type = loginMode.value === 'email' ? 2 : 1;\n await session.loginWithPassword(loginAccount.value, loginPassword.value, type);\n }\n function switchRegisterType(type) {\n registerType.value = type;\n registerForm.number = '';\n registerForm.code = '';\n registerForm.password = '';\n registerForm.confirmPassword = '';\n registerForm.invitationCode = '';\n clearFormMessage();\n }\n async function requestEmailCode() {\n clearFormMessage();\n sendingCode.value = true;\n try {\n await _services_imClient__WEBPACK_IMPORTED_MODULE_6__.imClient.requestAuthCode(registerForm.number);\n info.value = '邮箱验证码已发送,请在当前注册表单中填写后继续。';\n } catch (error) {\n session.error = error.message || '验证码发送失败';\n } finally {\n sendingCode.value = false;\n }\n }\n async function submitRegister() {\n clearFormMessage();\n if (registerForm.password !== registerForm.confirmPassword) {\n session.error = '两次输入的密码不一致';\n return;\n }\n const result = await session.register({\n type: registerType.value,\n number: registerForm.number,\n code: registerType.value === 2 ? registerForm.code : '',\n password: registerForm.password,\n invitationCode: registerForm.invitationCode\n });\n if (result) {\n info.value = '注册成功,正在进入系统。';\n }\n }\n function openRegister() {\n clearFormMessage();\n stopQrLogin();\n view.value = 'register';\n }\n function backToLogin() {\n clearFormMessage();\n loginMode.value = 'phone';\n stopQrLogin();\n view.value = 'login';\n }\n function resetQrState() {\n qrStatus.value = 'idle';\n qrCodeImage.value = '';\n qrSessionToken.value = '';\n qrUserName.value = '';\n qrUserPortrait.value = '';\n }\n function clearQrExpireTimer() {\n if (qrExpireTimer) {\n window.clearTimeout(qrExpireTimer);\n qrExpireTimer = null;\n }\n }\n function stopQrLogin() {\n qrGeneration += 1;\n clearQrExpireTimer();\n resetQrState();\n }\n function refreshQrLogin() {\n startQrLogin(true);\n }\n async function startQrLogin(force = false) {\n if (loginMode.value !== 'qr') {\n return;\n }\n if (!force && qrSessionToken.value && qrCodeImage.value && !['expired', 'error'].includes(qrStatus.value)) {\n return;\n }\n qrGeneration += 1;\n const generation = qrGeneration;\n clearQrExpireTimer();\n clearFormMessage();\n qrStatus.value = 'loading';\n qrCodeImage.value = '';\n qrSessionToken.value = '';\n qrUserName.value = '';\n qrUserPortrait.value = '';\n try {\n const pcSession = await _services_imClient__WEBPACK_IMPORTED_MODULE_6__.imClient.createPcSession();\n if (generation !== qrGeneration || loginMode.value !== 'qr') {\n return;\n }\n const token = String(pcSession.token || '');\n if (!token) {\n throw new Error('服务端没有返回扫码 token');\n }\n qrSessionToken.value = token;\n qrCodeImage.value = jr_qrcode__WEBPACK_IMPORTED_MODULE_2___default().getQrBase64(QR_CODE_PREFIX_PC_SESSION + token);\n qrStatus.value = 'waiting';\n const expireMs = Math.max(30000, Math.min(Number(pcSession.expired || 300000), 300000));\n qrExpireTimer = window.setTimeout(() => {\n if (generation === qrGeneration && qrStatus.value !== 'confirmed' && !session.isAuthenticated) {\n qrStatus.value = 'expired';\n }\n }, expireMs);\n pollQrLogin(generation, token);\n } catch (error) {\n if (generation !== qrGeneration) {\n return;\n }\n qrStatus.value = 'error';\n session.error = error.message || '二维码加载失败';\n }\n }\n async function pollQrLogin(generation, token) {\n try {\n while (generation === qrGeneration && loginMode.value === 'qr' && !session.isAuthenticated) {\n const payload = await _services_imClient__WEBPACK_IMPORTED_MODULE_6__.imClient.loginWithPcSession(token);\n if (generation !== qrGeneration || loginMode.value !== 'qr') {\n return;\n }\n const code = Number(payload.code);\n const data = payload.data || payload.result || {};\n const status = Number(data.status);\n if (code === 0 && data.imToken) {\n qrStatus.value = 'confirmed';\n clearQrExpireTimer();\n await session.completePcSessionLogin(data);\n return;\n }\n if (code === 20042 || status === 1 || status === 3) {\n qrStatus.value = 'scanned';\n qrUserName.value = data.userName || data.displayName || qrUserName.value;\n qrUserPortrait.value = data.portrait || qrUserPortrait.value;\n await delay(1000);\n continue;\n }\n if (status === 0 || code === 13) {\n qrStatus.value = 'waiting';\n await delay(1000);\n continue;\n }\n if (status === 4) {\n qrStatus.value = 'expired';\n session.error = payload.message || '扫码登录已取消';\n return;\n }\n if (payload.message) {\n throw new Error(payload.message);\n }\n await delay(1000);\n }\n } catch (error) {\n if (generation === qrGeneration && loginMode.value === 'qr' && !session.isAuthenticated) {\n qrStatus.value = 'error';\n session.error = error.message || '扫码登录失败';\n }\n }\n }\n function delay(ms) {\n return new Promise(resolve => window.setTimeout(resolve, ms));\n }\n async function loadPlatformConfig() {\n try {\n const response = await axios__WEBPACK_IMPORTED_MODULE_1__[\"default\"].get(_config__WEBPACK_IMPORTED_MODULE_4__.appConfig.platformConfigApi, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '平台配置加载失败');\n }\n const config = response.data?.result || {};\n const name = String(config.platformName || '').trim();\n const logo = String(config.logoUrl || config.logo || '').trim();\n const version = String(config.version || '').trim();\n platformName.value = name || defaultPlatformName;\n platformLogo.value = resolveBrandLogo(logo, version);\n } catch (error) {\n platformName.value = defaultPlatformName;\n platformLogo.value = defaultPlatformLogo;\n }\n applyDocumentBrand();\n }\n function resolveBrandLogo(value, version = '') {\n const logo = String(value || '').trim();\n if (!logo) {\n return defaultPlatformLogo;\n }\n if (/^(https?:|data:|blob:)/i.test(logo)) {\n return appendBrandVersion(logo, version);\n }\n if (logo.startsWith('//')) {\n return appendBrandVersion(`${window.location.protocol}${logo}`, version);\n }\n const base = logo.startsWith('/') ? _config__WEBPACK_IMPORTED_MODULE_4__.appConfig.adminServer : `${_config__WEBPACK_IMPORTED_MODULE_4__.appConfig.adminServer}/`;\n return appendBrandVersion(new URL(logo, base).toString(), version);\n }\n function appendBrandVersion(url, version) {\n if (!version || /^(data:|blob:)/i.test(url)) {\n return url;\n }\n const parsed = new URL(url, window.location.href);\n parsed.searchParams.set('v', version);\n return parsed.toString();\n }\n function applyDocumentBrand() {\n if (typeof document === 'undefined') {\n return;\n }\n document.title = platformName.value || defaultPlatformName;\n let icon = document.querySelector('link[rel=\"icon\"]');\n if (!icon) {\n icon = document.createElement('link');\n icon.setAttribute('rel', 'icon');\n document.head.appendChild(icon);\n }\n icon.setAttribute('href', platformLogo.value || defaultPlatformLogo);\n }\n function handleBrandLogoError() {\n if (platformLogo.value !== defaultPlatformLogo) {\n platformLogo.value = defaultPlatformLogo;\n applyDocumentBrand();\n }\n }\n const __returned__ = {\n session,\n view,\n loginMode,\n loginAccount,\n loginPassword,\n registerType,\n registerForm,\n sendingCode,\n info,\n QR_CODE_PREFIX_PC_SESSION,\n qrStatus,\n qrCodeImage,\n qrSessionToken,\n qrUserName,\n qrUserPortrait,\n get qrGeneration() {\n return qrGeneration;\n },\n set qrGeneration(v) {\n qrGeneration = v;\n },\n get qrExpireTimer() {\n return qrExpireTimer;\n },\n set qrExpireTimer(v) {\n qrExpireTimer = v;\n },\n defaultPlatformName,\n defaultPlatformLogo,\n platformName,\n platformLogo,\n canSubmitLogin,\n canRequestEmailCode,\n canSubmitRegister,\n qrUserInitial,\n qrStatusTitle,\n qrStatusText,\n qrRefreshText,\n clearFormMessage,\n setLoginMode,\n submitLogin,\n switchRegisterType,\n requestEmailCode,\n submitRegister,\n openRegister,\n backToLogin,\n resetQrState,\n clearQrExpireTimer,\n stopQrLogin,\n refreshQrLogin,\n startQrLogin,\n pollQrLogin,\n delay,\n loadPlatformConfig,\n resolveBrandLogo,\n appendBrandVersion,\n applyDocumentBrand,\n handleBrandLogoError,\n get axios() {\n return axios__WEBPACK_IMPORTED_MODULE_1__[\"default\"];\n },\n get jrQRCode() {\n return (jr_qrcode__WEBPACK_IMPORTED_MODULE_2___default());\n },\n computed: vue__WEBPACK_IMPORTED_MODULE_3__.computed,\n onBeforeUnmount: vue__WEBPACK_IMPORTED_MODULE_3__.onBeforeUnmount,\n onMounted: vue__WEBPACK_IMPORTED_MODULE_3__.onMounted,\n reactive: vue__WEBPACK_IMPORTED_MODULE_3__.reactive,\n ref: vue__WEBPACK_IMPORTED_MODULE_3__.ref,\n get appConfig() {\n return _config__WEBPACK_IMPORTED_MODULE_4__.appConfig;\n },\n get useSessionStore() {\n return _stores_session__WEBPACK_IMPORTED_MODULE_5__.useSessionStore;\n },\n get imClient() {\n return _services_imClient__WEBPACK_IMPORTED_MODULE_6__.imClient;\n }\n };\n Object.defineProperty(__returned__, '__isScriptSetup', {\n enumerable: false,\n value: true\n });\n return __returned__;\n }\n});\n\n//# sourceURL=webpack://jc-web-im/./src/components/LoginView.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/ChatPanel.vue?vue&type=script&setup=true&lang=js": +/*!***********************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/ChatPanel.vue?vue&type=script&setup=true&lang=js ***! + \***********************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.includes.js */ \"./node_modules/core-js/modules/es.array.includes.js\");\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.iterator.constructor.js */ \"./node_modules/core-js/modules/es.iterator.constructor.js\");\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/es.iterator.filter.js */ \"./node_modules/core-js/modules/es.iterator.filter.js\");\n/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! core-js/modules/es.iterator.find.js */ \"./node_modules/core-js/modules/es.iterator.find.js\");\n/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! core-js/modules/es.iterator.map.js */ \"./node_modules/core-js/modules/es.iterator.map.js\");\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_4__);\n/* harmony import */ var core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! core-js/modules/es.set.difference.v2.js */ \"./node_modules/core-js/modules/es.set.difference.v2.js\");\n/* harmony import */ var core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! core-js/modules/es.set.intersection.v2.js */ \"./node_modules/core-js/modules/es.set.intersection.v2.js\");\n/* harmony import */ var core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_6__);\n/* harmony import */ var core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! core-js/modules/es.set.is-disjoint-from.v2.js */ \"./node_modules/core-js/modules/es.set.is-disjoint-from.v2.js\");\n/* harmony import */ var core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_7__);\n/* harmony import */ var core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! core-js/modules/es.set.is-subset-of.v2.js */ \"./node_modules/core-js/modules/es.set.is-subset-of.v2.js\");\n/* harmony import */ var core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_8__);\n/* harmony import */ var core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! core-js/modules/es.set.is-superset-of.v2.js */ \"./node_modules/core-js/modules/es.set.is-superset-of.v2.js\");\n/* harmony import */ var core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_9___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_9__);\n/* harmony import */ var core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! core-js/modules/es.set.symmetric-difference.v2.js */ \"./node_modules/core-js/modules/es.set.symmetric-difference.v2.js\");\n/* harmony import */ var core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_10___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_10__);\n/* harmony import */ var core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! core-js/modules/es.set.union.v2.js */ \"./node_modules/core-js/modules/es.set.union.v2.js\");\n/* harmony import */ var core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_11___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_11__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n/* harmony import */ var _MessageList_vue__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! ./MessageList.vue */ \"./src/components/chat/MessageList.vue\");\n/* harmony import */ var _MessageComposer_vue__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(/*! ./MessageComposer.vue */ \"./src/components/chat/MessageComposer.vue\");\n/* harmony import */ var _services_imClient__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(/*! @/services/imClient */ \"./src/services/imClient.js\");\n/* harmony import */ var _stores_conversations__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(/*! @/stores/conversations */ \"./src/stores/conversations.js\");\n/* harmony import */ var _stores_contacts__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(/*! @/stores/contacts */ \"./src/stores/contacts.js\");\n/* harmony import */ var _stores_session__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(/*! @/stores/session */ \"./src/stores/session.js\");\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n __name: 'ChatPanel',\n setup(__props, {\n expose: __expose\n }) {\n __expose();\n const conversations = (0,_stores_conversations__WEBPACK_IMPORTED_MODULE_16__.useConversationStore)();\n const contacts = (0,_stores_contacts__WEBPACK_IMPORTED_MODULE_17__.useContactStore)();\n const session = (0,_stores_session__WEBPACK_IMPORTED_MODULE_18__.useSessionStore)();\n const activeConversation = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => conversations.activeConversation);\n const groupMenuOpen = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)(false);\n const singleProfileOpen = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)(false);\n const singleAliasDraft = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)('');\n const singleProfileSaving = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)(false);\n const singleProfileNotice = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)('');\n const singleProfileError = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)('');\n const groupPanel = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)('');\n const groupBusy = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)(false);\n const groupDetail = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)(null);\n const groupMembers = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)([]);\n const groupAliasDraft = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)('');\n const manageSection = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)('');\n const selectedAdminIds = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)([]);\n const selectedKickIds = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)([]);\n const transferTargetId = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)('');\n const profileDraft = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)({\n name: '',\n announcement: '',\n welcomeText: ''\n });\n const muteDraft = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)({\n mode: 'off',\n scheduleType: 'once',\n startsAtLocal: '',\n endsAtLocal: '',\n dailyStart: '09:00',\n dailyEnd: '18:00'\n });\n const muteSaveNotice = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)('');\n const muteSaveError = (0,vue__WEBPACK_IMPORTED_MODULE_12__.ref)('');\n const singlePeerId = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => {\n if (!activeConversation.value || isGroupConversation.value) {\n return '';\n }\n return String(activeConversation.value.target || activeConversation.value.raw?.target || activeConversation.value.id?.replace(/^single-/, '') || '');\n });\n const singleContact = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => {\n const peerId = singlePeerId.value;\n if (!peerId) {\n return null;\n }\n return contacts.contacts.find(contact => String(contact.uid || contact.id || '') === peerId) || null;\n });\n const singleProfile = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => {\n const contact = singleContact.value;\n const conversation = activeConversation.value || {};\n const rawUser = contact?.rawUser || {};\n const displayName = contact?.alias || rawUser.displayName || rawUser.name || contact?.title || conversation.title || singlePeerId.value || '未知用户';\n const rawName = rawUser.displayName || rawUser.name || contact?.title || conversation.title || '';\n return {\n uid: singlePeerId.value,\n displayName,\n rawName,\n mobile: rawUser.mobile || conversation.raw?.mobile || '',\n address: rawUser.address || conversation.raw?.address || '',\n portrait: contact?.portrait || conversation.portrait || '',\n avatarText: String(displayName || '?').slice(0, 1).toUpperCase(),\n alias: contact?.alias || ''\n };\n });\n const isGroupConversation = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => {\n return ['group', 'channel'].includes(activeConversation.value?.type) || activeConversation.value?.raw?.type === 'group';\n });\n const currentGroupRole = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => {\n return groupDetail.value?.myRole || activeConversation.value?.role || activeConversation.value?.raw?.role || '';\n });\n const isGroupOwner = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => currentGroupRole.value === 'owner');\n const canManageGroup = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => ['owner', 'admin'].includes(currentGroupRole.value));\n const adminCandidateMembers = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => groupMembers.value.filter(member => member.role !== 'owner'));\n const transferableMembers = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => groupMembers.value.filter(member => member.role !== 'owner'));\n const messageDetail = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => conversations.messageDetail || null);\n const messageDetailGroup = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => messageDetail.value?.group || null);\n const isMessageDetailGroup = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => {\n return messageDetail.value?.conversationType === 'group' || Boolean(messageDetail.value?.group);\n });\n const messageDetailCounts = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => {\n return messageDetailGroup.value?.counts || {\n read: 0,\n unread: 0,\n undelivered: 0,\n total: 0\n };\n });\n const kickCandidateMembers = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => {\n return groupMembers.value.filter(member => {\n if (member.uid === session.userId || member.role === 'owner') {\n return false;\n }\n if (currentGroupRole.value === 'admin') {\n return member.role === 'member';\n }\n return currentGroupRole.value === 'owner';\n });\n });\n const statusText = (0,vue__WEBPACK_IMPORTED_MODULE_12__.computed)(() => {\n if (activeConversation.value?.type === 'channel' && !conversations.canSendActiveConversation) {\n return '公告群,仅管理员可发言';\n }\n return session.connectionStatusText || (session.connected ? '已连接' : '未连接');\n });\n function formatDetailTime(value) {\n const timestamp = Number(value || 0);\n if (!timestamp) {\n return '暂无';\n }\n return new Intl.DateTimeFormat('zh-CN', {\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n hour12: false,\n timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone\n }).format(timestamp);\n }\n function detailMemberName(member) {\n return member?.name || member?.displayName || member?.rawName || member?.uid || '-';\n }\n function detailMemberAvatar(member) {\n return String(detailMemberName(member) || '?').slice(0, 1).toUpperCase();\n }\n function detailRoleLabel(role) {\n if (role === 'owner') {\n return '群主';\n }\n if (role === 'admin') {\n return '管理员';\n }\n return '';\n }\n function closeGroupMenu() {\n groupMenuOpen.value = false;\n }\n function toggleGroupMenu() {\n if (!isGroupConversation.value) {\n return;\n }\n groupMenuOpen.value = !groupMenuOpen.value;\n }\n async function openSingleProfile() {\n if (!activeConversation.value || isGroupConversation.value) {\n return;\n }\n closeGroupMenu();\n singleProfileNotice.value = '';\n singleProfileError.value = '';\n singleAliasDraft.value = singleProfile.value.alias || '';\n singleProfileOpen.value = true;\n contacts.refreshContacts().then(() => {\n if (singleProfileOpen.value) {\n singleAliasDraft.value = singleProfile.value.alias || '';\n }\n }).catch(() => {});\n }\n function closeSingleProfile() {\n if (singleProfileSaving.value) {\n return;\n }\n singleProfileOpen.value = false;\n singleProfileNotice.value = '';\n singleProfileError.value = '';\n }\n async function saveSingleAlias() {\n const peerId = singleProfile.value.uid;\n if (!peerId || singleProfileSaving.value) {\n return;\n }\n singleProfileSaving.value = true;\n singleProfileNotice.value = '';\n singleProfileError.value = '';\n try {\n const ok = await contacts.updateContactAlias(peerId, singleAliasDraft.value);\n if (!ok) {\n singleProfileError.value = contacts.actionError || '备注名修改失败';\n return;\n }\n await contacts.refreshContacts({\n force: true\n }).catch(() => {});\n const refreshedContact = contacts.contacts.find(contact => String(contact.uid || contact.id || '') === peerId) || singleContact.value;\n const nextTitle = refreshedContact?.title || singleAliasDraft.value || singleProfile.value.rawName || singleProfile.value.displayName || peerId;\n conversations.upsertConversation({\n ...activeConversation.value,\n title: nextTitle,\n portrait: refreshedContact?.portrait || activeConversation.value?.portrait || '',\n avatarText: String(nextTitle || '?').slice(0, 1).toUpperCase(),\n raw: {\n ...(activeConversation.value?.raw || {}),\n target: peerId,\n mobile: refreshedContact?.rawUser?.mobile || activeConversation.value?.raw?.mobile || '',\n address: refreshedContact?.rawUser?.address || activeConversation.value?.raw?.address || '',\n alias: refreshedContact?.alias || singleAliasDraft.value\n }\n });\n singleAliasDraft.value = refreshedContact?.alias || singleAliasDraft.value;\n singleProfileNotice.value = '备注名已保存';\n } catch (error) {\n singleProfileError.value = error.message || '备注名修改失败';\n } finally {\n singleProfileSaving.value = false;\n }\n }\n function handleSenderProfileUpdated() {\n contacts.refreshContacts({\n force: true\n }).catch(() => {});\n conversations.syncActiveMessages({\n allowReadReceipt: true\n });\n if (groupPanel.value === 'members') {\n loadGroupMembers();\n }\n }\n function closeGroupPanel() {\n groupPanel.value = '';\n groupBusy.value = false;\n clearMuteSaveFeedback();\n }\n async function loadGroupDetail() {\n if (!session.userId || !activeConversation.value?.target) {\n return null;\n }\n groupBusy.value = true;\n try {\n groupDetail.value = await _services_imClient__WEBPACK_IMPORTED_MODULE_15__.imClient.getGroupDetail(session.userId, activeConversation.value.target);\n return groupDetail.value;\n } catch (error) {\n conversations.setActionError(error.message || '群资料加载失败');\n return null;\n } finally {\n groupBusy.value = false;\n }\n }\n async function openGroupOverview() {\n if (!isGroupConversation.value) {\n return;\n }\n closeGroupMenu();\n groupPanel.value = 'overview';\n await loadGroupDetail();\n }\n async function openGroupMembers() {\n closeGroupMenu();\n groupPanel.value = 'members';\n await loadGroupMembers();\n }\n async function loadGroupMembers() {\n groupBusy.value = true;\n try {\n groupMembers.value = await _services_imClient__WEBPACK_IMPORTED_MODULE_15__.imClient.getGroupMembers(session.userId, activeConversation.value.target);\n selectedAdminIds.value = groupMembers.value.filter(member => member.role === 'admin').map(member => member.uid);\n return groupMembers.value;\n } catch (error) {\n conversations.setActionError(error.message || '群员加载失败');\n return [];\n } finally {\n groupBusy.value = false;\n }\n }\n async function openGroupAlias() {\n closeGroupMenu();\n groupPanel.value = 'alias';\n const detail = await loadGroupDetail();\n groupAliasDraft.value = detail?.myAlias || '';\n }\n function msToLocalInput(value) {\n const timestamp = Number(value || 0);\n if (!timestamp) {\n return '';\n }\n const date = new Date(timestamp);\n const local = new Date(date.getTime() - date.getTimezoneOffset() * 60000);\n return local.toISOString().slice(0, 16);\n }\n function localInputToMs(value) {\n return value ? new Date(value).getTime() : 0;\n }\n function prepareManagementDrafts() {\n clearMuteSaveFeedback();\n profileDraft.value = {\n name: groupDetail.value?.name || activeConversation.value?.title || '',\n announcement: groupDetail.value?.announcement || '',\n welcomeText: groupDetail.value?.welcomeText || ''\n };\n const setting = groupDetail.value?.muteSetting || {};\n muteDraft.value = {\n mode: setting.mode || 'off',\n scheduleType: setting.scheduleType || 'once',\n startsAtLocal: msToLocalInput(setting.startsAtMs),\n endsAtLocal: msToLocalInput(setting.endsAtMs),\n dailyStart: setting.dailyStart || '09:00',\n dailyEnd: setting.dailyEnd || '18:00'\n };\n }\n function clearMuteSaveFeedback() {\n muteSaveNotice.value = '';\n muteSaveError.value = '';\n }\n async function openGroupManage() {\n if (!canManageGroup.value) {\n return;\n }\n closeGroupMenu();\n groupPanel.value = 'manage';\n manageSection.value = isGroupOwner.value ? 'admins' : 'profile';\n await loadGroupDetail();\n prepareManagementDrafts();\n await loadGroupMembers();\n }\n async function openTransferGroup() {\n groupPanel.value = 'transfer';\n transferTargetId.value = '';\n await loadGroupMembers();\n }\n async function saveGroupAlias() {\n if (!activeConversation.value?.target || groupBusy.value) {\n return;\n }\n groupBusy.value = true;\n try {\n const alias = await _services_imClient__WEBPACK_IMPORTED_MODULE_15__.imClient.updateGroupAlias(session.userId, activeConversation.value.target, groupAliasDraft.value);\n groupDetail.value = {\n ...(groupDetail.value || {}),\n myAlias: alias\n };\n conversations.setActionNotice('本群昵称已保存');\n closeGroupPanel();\n } catch (error) {\n conversations.setActionError(error.message || '本群昵称保存失败');\n } finally {\n groupBusy.value = false;\n }\n }\n function toggleAdmin(userId) {\n const selected = new Set(selectedAdminIds.value);\n if (selected.has(userId)) {\n selected.delete(userId);\n } else {\n selected.add(userId);\n }\n selectedAdminIds.value = Array.from(selected);\n }\n function toggleKick(userId) {\n const selected = new Set(selectedKickIds.value);\n if (selected.has(userId)) {\n selected.delete(userId);\n } else {\n selected.add(userId);\n }\n selectedKickIds.value = Array.from(selected);\n }\n async function saveGroupAdmins() {\n if (!activeConversation.value?.target || groupBusy.value) {\n return;\n }\n groupBusy.value = true;\n try {\n await _services_imClient__WEBPACK_IMPORTED_MODULE_15__.imClient.setGroupAdmins(session.userId, activeConversation.value.target, selectedAdminIds.value);\n groupMembers.value = groupMembers.value.map(member => {\n if (member.role === 'owner') {\n return member;\n }\n return {\n ...member,\n role: selectedAdminIds.value.includes(member.uid) ? 'admin' : 'member'\n };\n });\n conversations.setActionNotice('管理员已更新');\n } catch (error) {\n conversations.setActionError(error.message || '管理员设置失败');\n } finally {\n groupBusy.value = false;\n }\n }\n async function saveGroupProfile() {\n if (!activeConversation.value?.target || groupBusy.value) {\n return;\n }\n groupBusy.value = true;\n try {\n const result = await _services_imClient__WEBPACK_IMPORTED_MODULE_15__.imClient.updateGroupProfile(session.userId, activeConversation.value.target, profileDraft.value);\n groupDetail.value = {\n ...(groupDetail.value || {}),\n name: result.name,\n announcement: result.announcement,\n welcomeText: result.welcomeText\n };\n conversations.upsertConversation({\n ...activeConversation.value,\n title: result.name,\n avatarText: String(result.name || '?').slice(0, 1).toUpperCase(),\n updatedAt: Number(result.updatedAt || Date.now())\n });\n conversations.setActionNotice('群资料已保存');\n } catch (error) {\n conversations.setActionError(error.message || '群资料保存失败');\n } finally {\n groupBusy.value = false;\n }\n }\n async function kickSelectedMembers() {\n if (!selectedKickIds.value.length || groupBusy.value) {\n return;\n }\n if (!window.confirm('确认将所选成员移出群组?')) {\n return;\n }\n groupBusy.value = true;\n try {\n const result = await _services_imClient__WEBPACK_IMPORTED_MODULE_15__.imClient.kickGroupMembers(session.userId, activeConversation.value.target, selectedKickIds.value);\n const removed = new Set(result.removedIds || selectedKickIds.value);\n groupMembers.value = groupMembers.value.filter(member => !removed.has(member.uid));\n selectedKickIds.value = [];\n conversations.setActionNotice('已移出群组');\n } catch (error) {\n conversations.setActionError(error.message || '踢人失败');\n } finally {\n groupBusy.value = false;\n }\n }\n async function saveMuteSetting() {\n if (!activeConversation.value?.target || groupBusy.value) {\n return;\n }\n clearMuteSaveFeedback();\n groupBusy.value = true;\n try {\n const setting = await _services_imClient__WEBPACK_IMPORTED_MODULE_15__.imClient.updateGroupMuteSetting(session.userId, activeConversation.value.target, {\n mode: muteDraft.value.mode,\n scheduleType: muteDraft.value.scheduleType,\n startsAtMs: localInputToMs(muteDraft.value.startsAtLocal),\n endsAtMs: localInputToMs(muteDraft.value.endsAtLocal),\n dailyStart: muteDraft.value.dailyStart,\n dailyEnd: muteDraft.value.dailyEnd\n });\n groupDetail.value = {\n ...(groupDetail.value || {}),\n muteSetting: setting\n };\n muteSaveNotice.value = '禁言设置已保存';\n } catch (error) {\n muteSaveError.value = error.message || '禁言设置保存失败';\n } finally {\n groupBusy.value = false;\n }\n }\n async function leaveCurrentGroup() {\n if (!activeConversation.value || groupBusy.value) {\n return;\n }\n if (!window.confirm('确认退出该群组?')) {\n return;\n }\n groupBusy.value = true;\n const ok = await conversations.leaveGroup(activeConversation.value);\n groupBusy.value = false;\n if (ok) {\n closeGroupPanel();\n }\n }\n async function dismissCurrentGroup() {\n if (!activeConversation.value || groupBusy.value) {\n return;\n }\n if (!window.confirm('确认解散该群组?所有成员都将不可继续使用该群。')) {\n return;\n }\n groupBusy.value = true;\n const ok = await conversations.dismissGroup(activeConversation.value);\n groupBusy.value = false;\n if (ok) {\n closeGroupPanel();\n }\n }\n async function transferCurrentGroup() {\n if (!activeConversation.value?.target || !transferTargetId.value || groupBusy.value) {\n return;\n }\n if (!window.confirm('确认将群主转让给该成员?')) {\n return;\n }\n groupBusy.value = true;\n try {\n await _services_imClient__WEBPACK_IMPORTED_MODULE_15__.imClient.transferGroup(session.userId, activeConversation.value.target, transferTargetId.value);\n groupDetail.value = {\n ...(groupDetail.value || {}),\n myRole: 'member'\n };\n activeConversation.value.role = 'member';\n if (activeConversation.value.raw) {\n activeConversation.value.raw.role = 'member';\n }\n conversations.setActionNotice('群组已转让');\n closeGroupPanel();\n conversations.refreshRemoteConversations();\n } catch (error) {\n conversations.setActionError(error.message || '转让群组失败');\n } finally {\n groupBusy.value = false;\n }\n }\n function memberAvatar(member) {\n return String(member.displayName || member.uid || '?').slice(0, 1).toUpperCase();\n }\n document.addEventListener('click', closeGroupMenu);\n (0,vue__WEBPACK_IMPORTED_MODULE_12__.onBeforeUnmount)(() => {\n document.removeEventListener('click', closeGroupMenu);\n });\n (0,vue__WEBPACK_IMPORTED_MODULE_12__.watch)(() => activeConversation.value?.id, () => {\n closeGroupMenu();\n closeSingleProfile();\n closeGroupPanel();\n groupDetail.value = null;\n groupMembers.value = [];\n groupAliasDraft.value = '';\n selectedAdminIds.value = [];\n selectedKickIds.value = [];\n transferTargetId.value = '';\n manageSection.value = '';\n });\n const __returned__ = {\n conversations,\n contacts,\n session,\n activeConversation,\n groupMenuOpen,\n singleProfileOpen,\n singleAliasDraft,\n singleProfileSaving,\n singleProfileNotice,\n singleProfileError,\n groupPanel,\n groupBusy,\n groupDetail,\n groupMembers,\n groupAliasDraft,\n manageSection,\n selectedAdminIds,\n selectedKickIds,\n transferTargetId,\n profileDraft,\n muteDraft,\n muteSaveNotice,\n muteSaveError,\n singlePeerId,\n singleContact,\n singleProfile,\n isGroupConversation,\n currentGroupRole,\n isGroupOwner,\n canManageGroup,\n adminCandidateMembers,\n transferableMembers,\n messageDetail,\n messageDetailGroup,\n isMessageDetailGroup,\n messageDetailCounts,\n kickCandidateMembers,\n statusText,\n formatDetailTime,\n detailMemberName,\n detailMemberAvatar,\n detailRoleLabel,\n closeGroupMenu,\n toggleGroupMenu,\n openSingleProfile,\n closeSingleProfile,\n saveSingleAlias,\n handleSenderProfileUpdated,\n closeGroupPanel,\n loadGroupDetail,\n openGroupOverview,\n openGroupMembers,\n loadGroupMembers,\n openGroupAlias,\n msToLocalInput,\n localInputToMs,\n prepareManagementDrafts,\n clearMuteSaveFeedback,\n openGroupManage,\n openTransferGroup,\n saveGroupAlias,\n toggleAdmin,\n toggleKick,\n saveGroupAdmins,\n saveGroupProfile,\n kickSelectedMembers,\n saveMuteSetting,\n leaveCurrentGroup,\n dismissCurrentGroup,\n transferCurrentGroup,\n memberAvatar,\n computed: vue__WEBPACK_IMPORTED_MODULE_12__.computed,\n onBeforeUnmount: vue__WEBPACK_IMPORTED_MODULE_12__.onBeforeUnmount,\n ref: vue__WEBPACK_IMPORTED_MODULE_12__.ref,\n watch: vue__WEBPACK_IMPORTED_MODULE_12__.watch,\n MessageList: _MessageList_vue__WEBPACK_IMPORTED_MODULE_13__[\"default\"],\n MessageComposer: _MessageComposer_vue__WEBPACK_IMPORTED_MODULE_14__[\"default\"],\n get imClient() {\n return _services_imClient__WEBPACK_IMPORTED_MODULE_15__.imClient;\n },\n get useConversationStore() {\n return _stores_conversations__WEBPACK_IMPORTED_MODULE_16__.useConversationStore;\n },\n get useContactStore() {\n return _stores_contacts__WEBPACK_IMPORTED_MODULE_17__.useContactStore;\n },\n get useSessionStore() {\n return _stores_session__WEBPACK_IMPORTED_MODULE_18__.useSessionStore;\n }\n };\n Object.defineProperty(__returned__, '__isScriptSetup', {\n enumerable: false,\n value: true\n });\n return __returned__;\n }\n});\n\n//# sourceURL=webpack://jc-web-im/./src/components/chat/ChatPanel.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/MessageComposer.vue?vue&type=script&setup=true&lang=js": +/*!*****************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/MessageComposer.vue?vue&type=script&setup=true&lang=js ***! + \*****************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.includes.js */ \"./node_modules/core-js/modules/es.array.includes.js\");\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.iterator.constructor.js */ \"./node_modules/core-js/modules/es.iterator.constructor.js\");\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/es.iterator.filter.js */ \"./node_modules/core-js/modules/es.iterator.filter.js\");\n/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! core-js/modules/es.iterator.for-each.js */ \"./node_modules/core-js/modules/es.iterator.for-each.js\");\n/* harmony import */ var core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! core-js/modules/es.iterator.map.js */ \"./node_modules/core-js/modules/es.iterator.map.js\");\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_4__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n\n\n\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n __name: 'MessageComposer',\n props: {\n disabled: {\n type: Boolean,\n default: false\n },\n quoteMessage: {\n type: Object,\n default: null\n }\n },\n emits: ['send', 'send-attachment', 'cancel-quote'],\n setup(__props, {\n expose: __expose,\n emit: __emit\n }) {\n __expose();\n const props = __props;\n const emit = __emit;\n const text = (0,vue__WEBPACK_IMPORTED_MODULE_5__.ref)('');\n const emojiOpen = (0,vue__WEBPACK_IMPORTED_MODULE_5__.ref)(false);\n const dropActive = (0,vue__WEBPACK_IMPORTED_MODULE_5__.ref)(false);\n const textareaEl = (0,vue__WEBPACK_IMPORTED_MODULE_5__.ref)(null);\n const imageInput = (0,vue__WEBPACK_IMPORTED_MODULE_5__.ref)(null);\n const fileInput = (0,vue__WEBPACK_IMPORTED_MODULE_5__.ref)(null);\n let dragDepth = 0;\n const emojiList = ['😀', '😄', '😂', '😊', '😍', '😘', '😎', '😢', '😡', '👍', '🙏', '👏', '💪', '🎉', '❤️', '🔥', '🌹', '☕', '✅', '⭐', '💰', '🎁', '🤝', '👌'];\n function submit() {\n const value = text.value.trim();\n if (!value) {\n return;\n }\n emit('send', value);\n text.value = '';\n emojiOpen.value = false;\n }\n function toggleEmojiPicker() {\n emojiOpen.value = !emojiOpen.value;\n }\n function insertEmoji(emoji) {\n const element = textareaEl.value;\n if (!element) {\n text.value += emoji;\n return;\n }\n const start = element.selectionStart ?? text.value.length;\n const end = element.selectionEnd ?? text.value.length;\n text.value = `${text.value.slice(0, start)}${emoji}${text.value.slice(end)}`;\n requestAnimationFrame(() => {\n element.focus();\n const cursor = start + emoji.length;\n element.setSelectionRange(cursor, cursor);\n });\n }\n function chooseImage() {\n imageInput.value?.click();\n }\n function chooseFile() {\n fileInput.value?.click();\n }\n function attachmentTypeForFile(file, fallbackType = 'file') {\n if (file?.type?.startsWith('image/')) {\n return 'image';\n }\n return fallbackType === 'image' ? 'image' : 'file';\n }\n function emitFiles(files, fallbackType = 'file') {\n if (props.disabled) {\n return;\n }\n const validFiles = Array.from(files || []).filter(file => file instanceof File && file.size > 0);\n if (!validFiles.length) {\n return;\n }\n const caption = text.value.trim();\n validFiles.forEach((file, index) => {\n emit('send-attachment', {\n type: attachmentTypeForFile(file, fallbackType),\n file,\n text: index === 0 ? caption : ''\n });\n });\n text.value = '';\n emojiOpen.value = false;\n }\n function selectAttachment(event, type) {\n const input = event.target;\n const files = input.files ? Array.from(input.files) : [];\n input.value = '';\n if (!files.length || props.disabled) {\n return;\n }\n emitFiles(files, type);\n }\n function hasDraggedFiles(event) {\n const types = Array.from(event.dataTransfer?.types || []);\n return types.includes('Files');\n }\n function handleDragEnter(event) {\n if (props.disabled || !hasDraggedFiles(event)) {\n return;\n }\n dragDepth += 1;\n dropActive.value = true;\n event.dataTransfer.dropEffect = 'copy';\n }\n function handleDragOver(event) {\n if (props.disabled || !hasDraggedFiles(event)) {\n return;\n }\n dropActive.value = true;\n event.dataTransfer.dropEffect = 'copy';\n }\n function handleDragLeave() {\n dragDepth = Math.max(0, dragDepth - 1);\n if (dragDepth === 0) {\n dropActive.value = false;\n }\n }\n function filesFromDataTransfer(dataTransfer) {\n const items = Array.from(dataTransfer?.items || []);\n const files = items.filter(item => item.kind === 'file' && !item.webkitGetAsEntry?.()?.isDirectory).map(item => item.getAsFile()).filter(Boolean);\n if (files.length) {\n return files;\n }\n return Array.from(dataTransfer?.files || []);\n }\n function handleDrop(event) {\n dragDepth = 0;\n dropActive.value = false;\n if (props.disabled) {\n return;\n }\n emitFiles(filesFromDataTransfer(event.dataTransfer));\n }\n function imageFilesFromClipboard(clipboardData) {\n return Array.from(clipboardData?.items || []).filter(item => item.kind === 'file' && item.type.startsWith('image/')).map(item => item.getAsFile()).filter(Boolean);\n }\n function handlePaste(event) {\n if (props.disabled) {\n return;\n }\n const imageFiles = imageFilesFromClipboard(event.clipboardData);\n if (!imageFiles.length) {\n return;\n }\n event.preventDefault();\n emitFiles(imageFiles, 'image');\n }\n const __returned__ = {\n props,\n emit,\n text,\n emojiOpen,\n dropActive,\n textareaEl,\n imageInput,\n fileInput,\n get dragDepth() {\n return dragDepth;\n },\n set dragDepth(v) {\n dragDepth = v;\n },\n emojiList,\n submit,\n toggleEmojiPicker,\n insertEmoji,\n chooseImage,\n chooseFile,\n attachmentTypeForFile,\n emitFiles,\n selectAttachment,\n hasDraggedFiles,\n handleDragEnter,\n handleDragOver,\n handleDragLeave,\n filesFromDataTransfer,\n handleDrop,\n imageFilesFromClipboard,\n handlePaste,\n ref: vue__WEBPACK_IMPORTED_MODULE_5__.ref\n };\n Object.defineProperty(__returned__, '__isScriptSetup', {\n enumerable: false,\n value: true\n });\n return __returned__;\n }\n});\n\n//# sourceURL=webpack://jc-web-im/./src/components/chat/MessageComposer.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/MessageList.vue?vue&type=script&setup=true&lang=js": +/*!*************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/MessageList.vue?vue&type=script&setup=true&lang=js ***! + \*************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.includes.js */ \"./node_modules/core-js/modules/es.array.includes.js\");\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.array.push.js */ \"./node_modules/core-js/modules/es.array.push.js\");\n/* harmony import */ var core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/es.iterator.constructor.js */ \"./node_modules/core-js/modules/es.iterator.constructor.js\");\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! core-js/modules/es.iterator.find.js */ \"./node_modules/core-js/modules/es.iterator.find.js\");\n/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! core-js/modules/es.iterator.map.js */ \"./node_modules/core-js/modules/es.iterator.map.js\");\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_4__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n/* harmony import */ var _stores_contacts__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! @/stores/contacts */ \"./src/stores/contacts.js\");\n/* harmony import */ var _services_mediaProxy__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! @/services/mediaProxy */ \"./src/services/mediaProxy.js\");\n\n\n\n\n\n\n\n\nconst IMAGE_MIN_SCALE = 0.5;\nconst IMAGE_MAX_SCALE = 6;\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n __name: 'MessageList',\n props: {\n messages: {\n type: Array,\n default: () => []\n },\n selectionMode: {\n type: Boolean,\n default: false\n },\n selectedMessageIds: {\n type: Array,\n default: () => []\n },\n favoriteMessageIds: {\n type: Array,\n default: () => []\n },\n attentionMessageId: {\n type: String,\n default: ''\n },\n canRecallOthers: {\n type: Boolean,\n default: false\n }\n },\n emits: ['message-action', 'toggle-select', 'sender-profile-updated'],\n setup(__props, {\n expose: __expose,\n emit: __emit\n }) {\n __expose();\n const props = __props;\n const emit = __emit;\n const contacts = (0,_stores_contacts__WEBPACK_IMPORTED_MODULE_6__.useContactStore)();\n const listEl = (0,vue__WEBPACK_IMPORTED_MODULE_5__.ref)(null);\n const viewerStage = (0,vue__WEBPACK_IMPORTED_MODULE_5__.ref)(null);\n const contextMenu = (0,vue__WEBPACK_IMPORTED_MODULE_5__.reactive)({\n visible: false,\n x: 0,\n y: 0,\n message: null\n });\n const imageViewer = (0,vue__WEBPACK_IMPORTED_MODULE_5__.reactive)({\n visible: false,\n url: '',\n name: '',\n scale: 1,\n rotation: 0,\n offsetX: 0,\n offsetY: 0,\n dragging: false,\n dragStartX: 0,\n dragStartY: 0,\n dragOffsetX: 0,\n dragOffsetY: 0\n });\n const senderProfile = (0,vue__WEBPACK_IMPORTED_MODULE_5__.reactive)({\n visible: false,\n uid: '',\n displayName: '',\n rawName: '',\n mobile: '',\n address: '',\n portrait: '',\n avatarText: '',\n role: '',\n roleLabel: '',\n contactAlias: '',\n groupAlias: '',\n isContact: false,\n aliasDraft: '',\n saving: false,\n adding: false,\n notice: '',\n error: ''\n });\n const imageViewerStyle = (0,vue__WEBPACK_IMPORTED_MODULE_5__.computed)(() => ({\n transform: `translate3d(${imageViewer.offsetX}px, ${imageViewer.offsetY}px, 0) scale(${imageViewer.scale}) rotate(${imageViewer.rotation}deg)`\n }));\n const menuItems = (0,vue__WEBPACK_IMPORTED_MODULE_5__.computed)(() => {\n if (!contextMenu.message) {\n return [];\n }\n const isOwnMessage = contextMenu.message.direction === 'out';\n if (isRecalledMessage(contextMenu.message)) {\n return [...(isOwnMessage ? [{\n action: 'edit',\n label: '编辑'\n }] : []), {\n action: 'copy',\n label: '复制'\n }, {\n action: 'delete',\n label: '删除'\n }, {\n action: 'detail',\n label: '详情'\n }];\n }\n const favoriteLabel = isFavorite(contextMenu.message) ? '取消收藏' : '收藏';\n const items = [{\n action: 'copy',\n label: '复制'\n }, {\n action: 'delete',\n label: '删除'\n }, {\n action: 'forward',\n label: '转发'\n }, {\n action: 'favorite',\n label: favoriteLabel\n }, {\n action: 'quote',\n label: '引用'\n }, {\n action: 'select',\n label: '多选'\n }, {\n action: 'detail',\n label: '详情'\n }];\n if (isOwnMessage) {\n items.unshift({\n action: 'edit',\n label: '编辑'\n });\n items.push({\n action: 'recall',\n label: '撤回'\n });\n } else if (props.canRecallOthers) {\n items.push({\n action: 'recall',\n label: '撤回'\n });\n }\n return items;\n });\n function scrollToBottom() {\n (0,vue__WEBPACK_IMPORTED_MODULE_5__.nextTick)(() => {\n if (listEl.value) {\n listEl.value.scrollTop = listEl.value.scrollHeight;\n }\n });\n }\n function scrollToMessage(messageId) {\n (0,vue__WEBPACK_IMPORTED_MODULE_5__.nextTick)(() => {\n if (!listEl.value || !messageId) {\n return;\n }\n const row = Array.from(listEl.value.querySelectorAll('.message-row')).find(element => element.dataset.messageKey === messageId);\n if (row) {\n row.scrollIntoView({\n block: 'center',\n behavior: 'smooth'\n });\n }\n });\n }\n (0,vue__WEBPACK_IMPORTED_MODULE_5__.watch)(() => props.messages.map(message => `${message.id}:${message.status}:${message.deliveryStatus || ''}:${message.messageUid || ''}:${message.recalled ? '1' : ''}`).join('|'), scrollToBottom, {\n immediate: true\n });\n (0,vue__WEBPACK_IMPORTED_MODULE_5__.watch)(() => props.attentionMessageId, scrollToMessage);\n (0,vue__WEBPACK_IMPORTED_MODULE_5__.onMounted)(() => {\n document.addEventListener('click', closeMenu);\n document.addEventListener('keydown', closeMenuOnEscape);\n document.addEventListener('pointermove', moveImageDrag);\n document.addEventListener('pointerup', stopImageDrag);\n });\n (0,vue__WEBPACK_IMPORTED_MODULE_5__.onBeforeUnmount)(() => {\n document.removeEventListener('click', closeMenu);\n document.removeEventListener('keydown', closeMenuOnEscape);\n document.removeEventListener('pointermove', moveImageDrag);\n document.removeEventListener('pointerup', stopImageDrag);\n });\n function messageIdentity(message) {\n return String(message.messageUid || message.id || '');\n }\n function canOpenMessageMenu(message) {\n return message && message.status !== 'failed' && message.status !== 'sending';\n }\n function isSelected(message) {\n return props.selectedMessageIds.includes(messageIdentity(message));\n }\n function isFavorite(message) {\n return props.favoriteMessageIds.includes(messageIdentity(message));\n }\n function isAttention(message) {\n return props.attentionMessageId === messageIdentity(message);\n }\n function isRecallNotice(message) {\n return message.direction === 'out' && (message.recalled || message.status === 'recalled');\n }\n function isRecalledMessage(message) {\n return message.recalled || message.status === 'recalled';\n }\n function isImageMessage(message) {\n return message?.type === 'image' && message?.attachment?.url;\n }\n function isFileMessage(message) {\n return message?.type === 'file' && message?.attachment?.url;\n }\n function isGroupMessage(message) {\n return ['group', 'channel'].includes(message?.conversationType || '');\n }\n function showGroupSender(message) {\n return isGroupMessage(message) && message?.direction === 'in';\n }\n function senderRoleLabel(message) {\n const role = message?.senderProfile?.role || message?.senderRole || '';\n if (role === 'owner') {\n return '群主';\n }\n if (role === 'admin') {\n return '管理员';\n }\n return '';\n }\n function groupSenderName(message) {\n return message?.senderName || message?.senderProfile?.displayName || message?.from || '未知用户';\n }\n function groupSenderAvatarText(message) {\n return String(groupSenderName(message) || '?').slice(0, 1).toUpperCase();\n }\n function openSenderProfile(message) {\n const profile = message?.senderProfile || {};\n const uid = profile.uid || message?.from || '';\n const cachedContact = contacts.contacts.find(contact => String(contact.uid || contact.id || '') === uid) || null;\n const cachedAlias = String(cachedContact?.alias || '').trim();\n const contactAlias = String(profile.contactAlias || cachedAlias || profile.remarkAlias || '').trim();\n const groupAlias = String(profile.groupAlias || '').trim();\n const rawName = profile.rawName || cachedContact?.rawUser?.displayName || cachedContact?.rawUser?.name || cachedContact?.title || groupSenderName(message);\n const displayName = contactAlias || groupAlias || rawName || uid;\n senderProfile.visible = true;\n senderProfile.uid = uid;\n senderProfile.displayName = displayName;\n senderProfile.rawName = rawName || '';\n senderProfile.mobile = profile.mobile || cachedContact?.rawUser?.mobile || '';\n senderProfile.address = profile.address || cachedContact?.address || cachedContact?.rawUser?.address || '';\n senderProfile.portrait = profile.portrait || cachedContact?.portrait || message?.senderPortrait || '';\n senderProfile.avatarText = String(displayName || senderProfile.uid || '?').slice(0, 1).toUpperCase();\n senderProfile.role = profile.role || message?.senderRole || '';\n senderProfile.roleLabel = senderRoleLabel(message);\n senderProfile.contactAlias = contactAlias;\n senderProfile.groupAlias = groupAlias;\n senderProfile.isContact = Boolean(profile.isContact || cachedContact);\n senderProfile.aliasDraft = contactAlias;\n senderProfile.saving = false;\n senderProfile.adding = false;\n senderProfile.notice = '';\n senderProfile.error = '';\n closeMenu();\n }\n function closeSenderProfile() {\n if (senderProfile.saving || senderProfile.adding) {\n return;\n }\n senderProfile.visible = false;\n }\n async function addSenderAsContact() {\n if (!senderProfile.uid || senderProfile.adding || senderProfile.saving) {\n return;\n }\n senderProfile.adding = true;\n senderProfile.notice = '';\n senderProfile.error = '';\n const ok = await contacts.addContact(senderProfile.uid);\n if (ok || String(contacts.actionError || '').includes('已经是联系人')) {\n senderProfile.isContact = true;\n const alias = senderProfile.aliasDraft.trim();\n let aliasSaved = false;\n if (alias) {\n aliasSaved = await contacts.updateContactAlias(senderProfile.uid, alias);\n if (aliasSaved) {\n senderProfile.contactAlias = alias;\n senderProfile.displayName = alias;\n senderProfile.avatarText = String(alias || senderProfile.uid || '?').slice(0, 1).toUpperCase();\n }\n }\n await contacts.refreshContacts({\n force: true\n }).catch(() => {});\n if (alias && !aliasSaved) {\n senderProfile.notice = '';\n senderProfile.error = contacts.actionError ? `已添加为好友,但${contacts.actionError}` : '已添加为好友,但备注名同步失败';\n } else {\n senderProfile.notice = alias ? '已添加为好友,备注名已保存' : ok ? '已添加为好友' : '已是好友';\n }\n emit('sender-profile-updated', {\n userId: senderProfile.uid\n });\n } else {\n senderProfile.error = contacts.actionError || '添加好友失败';\n }\n senderProfile.adding = false;\n }\n async function saveSenderAlias() {\n if (!senderProfile.uid || senderProfile.saving || senderProfile.adding) {\n return;\n }\n senderProfile.saving = true;\n senderProfile.notice = '';\n senderProfile.error = '';\n const ok = await contacts.updateContactAlias(senderProfile.uid, senderProfile.aliasDraft);\n if (ok) {\n await contacts.refreshContacts({\n force: true\n }).catch(() => {});\n const alias = senderProfile.aliasDraft.trim();\n senderProfile.contactAlias = alias;\n senderProfile.displayName = alias || senderProfile.groupAlias || senderProfile.rawName || senderProfile.uid;\n senderProfile.avatarText = String(senderProfile.displayName || senderProfile.uid || '?').slice(0, 1).toUpperCase();\n senderProfile.notice = '备注名已保存';\n emit('sender-profile-updated', {\n userId: senderProfile.uid\n });\n } else {\n senderProfile.error = contacts.actionError || '备注名修改失败';\n }\n senderProfile.saving = false;\n }\n function roleBubbleClass(message) {\n const role = message?.senderRole || '';\n if (role === 'owner') {\n return 'role-bubble-owner';\n }\n if (role === 'admin') {\n return 'role-bubble-admin';\n }\n return '';\n }\n function formatFileSize(size) {\n const bytes = Number(size || 0);\n if (bytes >= 1024 * 1024) {\n return `${(bytes / 1024 / 1024).toFixed(bytes >= 10 * 1024 * 1024 ? 0 : 1)} MB`;\n }\n if (bytes >= 1024) {\n return `${(bytes / 1024).toFixed(bytes >= 10 * 1024 ? 0 : 1)} KB`;\n }\n return bytes > 0 ? `${bytes} B` : '';\n }\n function handleMessageClick(message) {\n if (props.selectionMode) {\n emit('toggle-select', message);\n }\n }\n function handleImageClick(message) {\n if (props.selectionMode) {\n emit('toggle-select', message);\n return;\n }\n openImageViewer(message);\n }\n function openImageViewer(message) {\n const attachment = message?.attachment || {};\n if (!attachment.url) {\n return;\n }\n closeMenu();\n imageViewer.visible = true;\n imageViewer.url = (0,_services_mediaProxy__WEBPACK_IMPORTED_MODULE_7__.imageDisplayUrl)(attachment.url);\n imageViewer.name = attachment.name || 'image';\n resetImageViewer();\n }\n function closeImageViewer() {\n imageViewer.visible = false;\n imageViewer.dragging = false;\n }\n function resetImageViewer() {\n imageViewer.scale = 1;\n imageViewer.rotation = 0;\n imageViewer.offsetX = 0;\n imageViewer.offsetY = 0;\n }\n function clampImageScale(value) {\n return Math.min(IMAGE_MAX_SCALE, Math.max(IMAGE_MIN_SCALE, Number(value.toFixed(2))));\n }\n function setImageScale(value, originX = null, originY = null) {\n const previousScale = imageViewer.scale;\n const nextScale = clampImageScale(value);\n if (nextScale === previousScale) {\n return;\n }\n if (originX !== null && originY !== null && viewerStage.value) {\n const rect = viewerStage.value.getBoundingClientRect();\n const centerX = rect.left + rect.width / 2;\n const centerY = rect.top + rect.height / 2;\n const ratio = nextScale / previousScale - 1;\n imageViewer.offsetX -= (originX - centerX - imageViewer.offsetX) * ratio;\n imageViewer.offsetY -= (originY - centerY - imageViewer.offsetY) * ratio;\n }\n imageViewer.scale = nextScale;\n if (nextScale <= 1) {\n imageViewer.offsetX = 0;\n imageViewer.offsetY = 0;\n }\n }\n function zoomImage(delta) {\n setImageScale(imageViewer.scale + delta);\n }\n function rotateImage() {\n imageViewer.rotation = (imageViewer.rotation + 90) % 360;\n }\n function handleImageWheel(event) {\n const factor = event.deltaY < 0 ? 1.12 : 1 / 1.12;\n setImageScale(imageViewer.scale * factor, event.clientX, event.clientY);\n }\n function startImageDrag(event) {\n if (!imageViewer.visible || imageViewer.scale <= 1) {\n return;\n }\n imageViewer.dragging = true;\n imageViewer.dragStartX = event.clientX;\n imageViewer.dragStartY = event.clientY;\n imageViewer.dragOffsetX = imageViewer.offsetX;\n imageViewer.dragOffsetY = imageViewer.offsetY;\n }\n function moveImageDrag(event) {\n if (!imageViewer.dragging) {\n return;\n }\n imageViewer.offsetX = imageViewer.dragOffsetX + event.clientX - imageViewer.dragStartX;\n imageViewer.offsetY = imageViewer.dragOffsetY + event.clientY - imageViewer.dragStartY;\n }\n function stopImageDrag() {\n imageViewer.dragging = false;\n }\n function saveViewerImage() {\n if (!imageViewer.url) {\n return;\n }\n const downloadUrl = new URL('/api/media_download.php', window.location.origin);\n downloadUrl.searchParams.set('url', imageViewer.url);\n downloadUrl.searchParams.set('name', imageViewer.name || 'image');\n const link = document.createElement('a');\n link.href = downloadUrl.toString();\n link.download = imageViewer.name || 'image';\n document.body.appendChild(link);\n link.click();\n link.remove();\n }\n function openMenu(event, message) {\n if (!canOpenMessageMenu(message)) {\n closeMenu();\n return;\n }\n contextMenu.visible = true;\n contextMenu.message = message;\n contextMenu.x = Math.min(event.clientX, window.innerWidth - 148);\n contextMenu.y = Math.min(event.clientY, window.innerHeight - 352);\n }\n function closeMenu() {\n contextMenu.visible = false;\n contextMenu.message = null;\n }\n function closeMenuOnEscape(event) {\n if (event.key === 'Escape') {\n if (senderProfile.visible) {\n closeSenderProfile();\n }\n if (imageViewer.visible) {\n closeImageViewer();\n }\n closeMenu();\n }\n }\n function chooseMenuAction(action) {\n if (contextMenu.message) {\n emit('message-action', {\n action,\n message: contextMenu.message\n });\n }\n closeMenu();\n }\n function formatTime(timestamp) {\n return new Intl.DateTimeFormat('zh-CN', {\n hour: '2-digit',\n minute: '2-digit',\n hour12: false,\n timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone\n }).format(timestamp);\n }\n function deliveryState(message) {\n if (message.direction !== 'out' || message.status === 'failed' || message.status === 'recalled' || message.recalled) {\n return '';\n }\n const state = message.deliveryStatus || message.status;\n if (state === 'read') {\n return 'read';\n }\n if (state === 'delivered') {\n return 'delivered';\n }\n if (state === 'sent') {\n return 'sent';\n }\n return '';\n }\n function deliveryTitle(message) {\n const state = deliveryState(message);\n if (state === 'read') {\n return '对方已读';\n }\n if (state === 'delivered') {\n return '已送达对方';\n }\n if (state === 'sent') {\n return '已送出';\n }\n return '';\n }\n function failureText(message) {\n if (message.status === 'failed') {\n if (message.errorCode) {\n return message.error ? `失败:${message.error}(${message.errorCode})` : `失败:${message.errorCode}`;\n }\n return message.error ? `失败:${message.error}` : '失败';\n }\n return '';\n }\n const __returned__ = {\n props,\n emit,\n contacts,\n listEl,\n viewerStage,\n contextMenu,\n IMAGE_MIN_SCALE,\n IMAGE_MAX_SCALE,\n imageViewer,\n senderProfile,\n imageViewerStyle,\n menuItems,\n scrollToBottom,\n scrollToMessage,\n messageIdentity,\n canOpenMessageMenu,\n isSelected,\n isFavorite,\n isAttention,\n isRecallNotice,\n isRecalledMessage,\n isImageMessage,\n isFileMessage,\n isGroupMessage,\n showGroupSender,\n senderRoleLabel,\n groupSenderName,\n groupSenderAvatarText,\n openSenderProfile,\n closeSenderProfile,\n addSenderAsContact,\n saveSenderAlias,\n roleBubbleClass,\n formatFileSize,\n handleMessageClick,\n handleImageClick,\n openImageViewer,\n closeImageViewer,\n resetImageViewer,\n clampImageScale,\n setImageScale,\n zoomImage,\n rotateImage,\n handleImageWheel,\n startImageDrag,\n moveImageDrag,\n stopImageDrag,\n saveViewerImage,\n openMenu,\n closeMenu,\n closeMenuOnEscape,\n chooseMenuAction,\n formatTime,\n deliveryState,\n deliveryTitle,\n failureText,\n computed: vue__WEBPACK_IMPORTED_MODULE_5__.computed,\n nextTick: vue__WEBPACK_IMPORTED_MODULE_5__.nextTick,\n onBeforeUnmount: vue__WEBPACK_IMPORTED_MODULE_5__.onBeforeUnmount,\n onMounted: vue__WEBPACK_IMPORTED_MODULE_5__.onMounted,\n reactive: vue__WEBPACK_IMPORTED_MODULE_5__.reactive,\n ref: vue__WEBPACK_IMPORTED_MODULE_5__.ref,\n watch: vue__WEBPACK_IMPORTED_MODULE_5__.watch,\n get useContactStore() {\n return _stores_contacts__WEBPACK_IMPORTED_MODULE_6__.useContactStore;\n },\n get imageDisplayUrl() {\n return _services_mediaProxy__WEBPACK_IMPORTED_MODULE_7__.imageDisplayUrl;\n }\n };\n Object.defineProperty(__returned__, '__isScriptSetup', {\n enumerable: false,\n value: true\n });\n return __returned__;\n }\n});\n\n//# sourceURL=webpack://jc-web-im/./src/components/chat/MessageList.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/contacts/ContactPanel.vue?vue&type=script&setup=true&lang=js": +/*!******************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/contacts/ContactPanel.vue?vue&type=script&setup=true&lang=js ***! + \******************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n/* harmony import */ var _stores_contacts__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @/stores/contacts */ \"./src/stores/contacts.js\");\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n __name: 'ContactPanel',\n setup(__props, {\n expose: __expose\n }) {\n __expose();\n const contacts = (0,_stores_contacts__WEBPACK_IMPORTED_MODULE_1__.useContactStore)();\n const contact = (0,vue__WEBPACK_IMPORTED_MODULE_0__.computed)(() => contacts.activeContact);\n const remarkName = (0,vue__WEBPACK_IMPORTED_MODULE_0__.computed)(() => contact.value?.alias || '未设置');\n const username = (0,vue__WEBPACK_IMPORTED_MODULE_0__.computed)(() => contact.value?.rawUser?.displayName || contact.value?.rawUser?.name || contact.value?.uid || '');\n const mobile = (0,vue__WEBPACK_IMPORTED_MODULE_0__.computed)(() => contact.value?.rawUser?.mobile || '未知');\n const address = (0,vue__WEBPACK_IMPORTED_MODULE_0__.computed)(() => contact.value?.rawUser?.address || '未知');\n const editingAlias = (0,vue__WEBPACK_IMPORTED_MODULE_0__.ref)(false);\n const aliasDraft = (0,vue__WEBPACK_IMPORTED_MODULE_0__.ref)('');\n const aliasSaving = (0,vue__WEBPACK_IMPORTED_MODULE_0__.ref)(false);\n const aliasError = (0,vue__WEBPACK_IMPORTED_MODULE_0__.ref)('');\n (0,vue__WEBPACK_IMPORTED_MODULE_0__.watch)(contact, () => {\n editingAlias.value = false;\n aliasError.value = '';\n });\n function openAliasEditor() {\n aliasDraft.value = contact.value?.alias || '';\n aliasError.value = '';\n editingAlias.value = true;\n }\n function closeAliasEditor() {\n if (aliasSaving.value) {\n return;\n }\n editingAlias.value = false;\n aliasError.value = '';\n }\n async function submitAlias() {\n if (!contact.value) {\n return;\n }\n aliasSaving.value = true;\n aliasError.value = '';\n const ok = await contacts.updateContactAlias(contact.value.uid, aliasDraft.value);\n aliasSaving.value = false;\n if (ok) {\n editingAlias.value = false;\n return;\n }\n aliasError.value = contacts.actionError || '备注名修改失败';\n }\n const __returned__ = {\n contacts,\n contact,\n remarkName,\n username,\n mobile,\n address,\n editingAlias,\n aliasDraft,\n aliasSaving,\n aliasError,\n openAliasEditor,\n closeAliasEditor,\n submitAlias,\n computed: vue__WEBPACK_IMPORTED_MODULE_0__.computed,\n ref: vue__WEBPACK_IMPORTED_MODULE_0__.ref,\n watch: vue__WEBPACK_IMPORTED_MODULE_0__.watch,\n get useContactStore() {\n return _stores_contacts__WEBPACK_IMPORTED_MODULE_1__.useContactStore;\n }\n };\n Object.defineProperty(__returned__, '__isScriptSetup', {\n enumerable: false,\n value: true\n });\n return __returned__;\n }\n});\n\n//# sourceURL=webpack://jc-web-im/./src/components/contacts/ContactPanel.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/favorites/FavoriteList.vue?vue&type=script&setup=true&lang=js": +/*!*******************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/favorites/FavoriteList.vue?vue&type=script&setup=true&lang=js ***! + \*******************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.iterator.constructor.js */ \"./node_modules/core-js/modules/es.iterator.constructor.js\");\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.iterator.find.js */ \"./node_modules/core-js/modules/es.iterator.find.js\");\n/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n/* harmony import */ var _stores_conversations__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @/stores/conversations */ \"./src/stores/conversations.js\");\n\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n __name: 'FavoriteList',\n setup(__props, {\n expose: __expose\n }) {\n __expose();\n const conversations = (0,_stores_conversations__WEBPACK_IMPORTED_MODULE_3__.useConversationStore)();\n const activeKey = (0,vue__WEBPACK_IMPORTED_MODULE_2__.computed)(() => conversations.activeFavorite?.key || '');\n function favoriteTitle(item) {\n if (item.conversationTitle) {\n return item.conversationTitle;\n }\n const conversation = conversations.conversations.find(entry => entry.id === item.conversationId);\n return conversation?.title || item.conversationId || '未知会话';\n }\n function favoritePreview(item) {\n if (item.recalled) {\n return `[${item.recallStatus || '已撤回'}]`;\n }\n if (item.preview) {\n return item.preview;\n }\n if (item.text) {\n return item.text;\n }\n if (item.type === 'image') {\n return '[图片]';\n }\n if (item.type === 'file') {\n return `[文件] ${item.attachment?.name || '文件'}`;\n }\n return '(无文字内容)';\n }\n function formatFavoriteTime(timestamp) {\n const value = Number(timestamp || 0);\n if (!value) {\n return '';\n }\n return new Intl.DateTimeFormat('zh-CN', {\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n hour12: false\n }).format(value);\n }\n const __returned__ = {\n conversations,\n activeKey,\n favoriteTitle,\n favoritePreview,\n formatFavoriteTime,\n computed: vue__WEBPACK_IMPORTED_MODULE_2__.computed,\n get useConversationStore() {\n return _stores_conversations__WEBPACK_IMPORTED_MODULE_3__.useConversationStore;\n }\n };\n Object.defineProperty(__returned__, '__isScriptSetup', {\n enumerable: false,\n value: true\n });\n return __returned__;\n }\n});\n\n//# sourceURL=webpack://jc-web-im/./src/components/favorites/FavoriteList.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/favorites/FavoritePanel.vue?vue&type=script&setup=true&lang=js": +/*!********************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/favorites/FavoritePanel.vue?vue&type=script&setup=true&lang=js ***! + \********************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.iterator.constructor.js */ \"./node_modules/core-js/modules/es.iterator.constructor.js\");\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.iterator.find.js */ \"./node_modules/core-js/modules/es.iterator.find.js\");\n/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n/* harmony import */ var _stores_conversations__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @/stores/conversations */ \"./src/stores/conversations.js\");\n/* harmony import */ var _stores_navigation__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @/stores/navigation */ \"./src/stores/navigation.js\");\n/* harmony import */ var _services_mediaProxy__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @/services/mediaProxy */ \"./src/services/mediaProxy.js\");\n\n\n\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n __name: 'FavoritePanel',\n setup(__props, {\n expose: __expose\n }) {\n __expose();\n const conversations = (0,_stores_conversations__WEBPACK_IMPORTED_MODULE_3__.useConversationStore)();\n const navigation = (0,_stores_navigation__WEBPACK_IMPORTED_MODULE_4__.useNavigationStore)();\n const favorite = (0,vue__WEBPACK_IMPORTED_MODULE_2__.computed)(() => conversations.activeFavorite);\n const notice = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)('');\n const error = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)('');\n function clearFeedback() {\n notice.value = '';\n error.value = '';\n }\n function favoriteTitle(item) {\n if (item.conversationTitle) {\n return item.conversationTitle;\n }\n const conversation = conversations.conversations.find(entry => entry.id === item.conversationId);\n return conversation?.title || item.conversationId || '未知会话';\n }\n function favoriteText(item) {\n return item.text || '';\n }\n function isImageFavorite(item) {\n return item?.type === 'image' && item?.attachment?.url;\n }\n function isFileFavorite(item) {\n return item?.type === 'file' && item?.attachment?.url;\n }\n function favoriteCopyText(item) {\n if (!item) {\n return '';\n }\n if (item.text) {\n return item.text;\n }\n if (item.recalled) {\n return item.recallStatus || '已撤回';\n }\n if (isImageFavorite(item)) {\n return item.attachment.url;\n }\n if (isFileFavorite(item)) {\n return `${item.attachment.name || '文件'} ${item.attachment.url}`.trim();\n }\n return item.preview || '';\n }\n function formatFileSize(size) {\n const bytes = Number(size || 0);\n if (bytes >= 1024 * 1024) {\n return `${(bytes / 1024 / 1024).toFixed(bytes >= 10 * 1024 * 1024 ? 0 : 1)} MB`;\n }\n if (bytes >= 1024) {\n return `${(bytes / 1024).toFixed(bytes >= 10 * 1024 ? 0 : 1)} KB`;\n }\n return bytes > 0 ? `${bytes} B` : '';\n }\n function formatFavoriteTime(timestamp) {\n const value = Number(timestamp || 0);\n if (!value) {\n return '';\n }\n return new Intl.DateTimeFormat('zh-CN', {\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit',\n hour12: false\n }).format(value);\n }\n async function copyFavorite() {\n clearFeedback();\n const text = favoriteCopyText(favorite.value || {}).trim();\n if (!text) {\n error.value = '没有可复制的文字内容';\n return;\n }\n try {\n await writeClipboard(text);\n notice.value = '已复制';\n } catch (copyError) {\n error.value = '复制失败';\n }\n }\n async function writeClipboard(text) {\n if (navigator.clipboard?.writeText) {\n await navigator.clipboard.writeText(text);\n return;\n }\n const element = document.createElement('textarea');\n element.value = text;\n element.setAttribute('readonly', 'readonly');\n element.style.position = 'fixed';\n element.style.left = '-9999px';\n document.body.appendChild(element);\n element.select();\n document.execCommand('copy');\n document.body.removeChild(element);\n }\n function openConversation() {\n clearFeedback();\n const item = favorite.value;\n if (!item?.conversationId) {\n error.value = '缺少原会话信息';\n return;\n }\n const conversation = conversations.conversations.find(entry => entry.id === item.conversationId);\n if (!conversation) {\n error.value = '原会话暂未加载,请刷新会话后再试';\n conversations.refreshRemoteConversations();\n return;\n }\n navigation.showMessages();\n conversations.setActiveConversation(item.conversationId);\n window.setTimeout(() => {\n conversations.focusMessage({\n id: item.id || item.key,\n messageUid: item.messageUid || item.key\n });\n }, 450);\n }\n function removeFavorite() {\n clearFeedback();\n const key = favorite.value?.key || '';\n if (!key) {\n return;\n }\n conversations.removeFavorite(key);\n notice.value = '已取消收藏';\n }\n const __returned__ = {\n conversations,\n navigation,\n favorite,\n notice,\n error,\n clearFeedback,\n favoriteTitle,\n favoriteText,\n isImageFavorite,\n isFileFavorite,\n favoriteCopyText,\n formatFileSize,\n formatFavoriteTime,\n copyFavorite,\n writeClipboard,\n openConversation,\n removeFavorite,\n computed: vue__WEBPACK_IMPORTED_MODULE_2__.computed,\n ref: vue__WEBPACK_IMPORTED_MODULE_2__.ref,\n get useConversationStore() {\n return _stores_conversations__WEBPACK_IMPORTED_MODULE_3__.useConversationStore;\n },\n get useNavigationStore() {\n return _stores_navigation__WEBPACK_IMPORTED_MODULE_4__.useNavigationStore;\n },\n get imageDisplayUrl() {\n return _services_mediaProxy__WEBPACK_IMPORTED_MODULE_5__.imageDisplayUrl;\n }\n };\n Object.defineProperty(__returned__, '__isScriptSetup', {\n enumerable: false,\n value: true\n });\n return __returned__;\n }\n});\n\n//# sourceURL=webpack://jc-web-im/./src/components/favorites/FavoritePanel.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/ContactList.vue?vue&type=script&setup=true&lang=js": +/*!****************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/ContactList.vue?vue&type=script&setup=true&lang=js ***! + \****************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.includes.js */ \"./node_modules/core-js/modules/es.array.includes.js\");\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.iterator.constructor.js */ \"./node_modules/core-js/modules/es.iterator.constructor.js\");\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/es.iterator.filter.js */ \"./node_modules/core-js/modules/es.iterator.filter.js\");\n/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n/* harmony import */ var _stores_contacts__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @/stores/contacts */ \"./src/stores/contacts.js\");\n\n\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n __name: 'ContactList',\n setup(__props, {\n expose: __expose\n }) {\n __expose();\n const contacts = (0,_stores_contacts__WEBPACK_IMPORTED_MODULE_4__.useContactStore)();\n const keyword = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)('');\n const addingContact = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)(false);\n const addKeyword = (0,vue__WEBPACK_IMPORTED_MODULE_3__.ref)('');\n const filteredContacts = (0,vue__WEBPACK_IMPORTED_MODULE_3__.computed)(() => {\n const value = keyword.value.toLowerCase();\n if (!value) {\n return contacts.contacts;\n }\n return contacts.contacts.filter(item => {\n return item.title.toLowerCase().includes(value) || item.subtitle.toLowerCase().includes(value) || item.uid.toLowerCase().includes(value);\n });\n });\n function openAddContact() {\n contacts.actionError = '';\n contacts.actionInfo = '';\n addKeyword.value = '';\n addingContact.value = true;\n }\n function closeAddContact() {\n addingContact.value = false;\n }\n async function submitAddContact() {\n const added = await contacts.addContact(addKeyword.value);\n if (added) {\n window.setTimeout(() => {\n addingContact.value = false;\n }, 500);\n }\n }\n const __returned__ = {\n contacts,\n keyword,\n addingContact,\n addKeyword,\n filteredContacts,\n openAddContact,\n closeAddContact,\n submitAddContact,\n computed: vue__WEBPACK_IMPORTED_MODULE_3__.computed,\n ref: vue__WEBPACK_IMPORTED_MODULE_3__.ref,\n get useContactStore() {\n return _stores_contacts__WEBPACK_IMPORTED_MODULE_4__.useContactStore;\n }\n };\n Object.defineProperty(__returned__, '__isScriptSetup', {\n enumerable: false,\n value: true\n });\n return __returned__;\n }\n});\n\n//# sourceURL=webpack://jc-web-im/./src/components/sidebar/ContactList.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/ConversationList.vue?vue&type=script&setup=true&lang=js": +/*!*********************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/ConversationList.vue?vue&type=script&setup=true&lang=js ***! + \*********************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.includes.js */ \"./node_modules/core-js/modules/es.array.includes.js\");\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.iterator.constructor.js */ \"./node_modules/core-js/modules/es.iterator.constructor.js\");\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/es.iterator.filter.js */ \"./node_modules/core-js/modules/es.iterator.filter.js\");\n/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var core_js_modules_es_iterator_flat_map_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! core-js/modules/es.iterator.flat-map.js */ \"./node_modules/core-js/modules/es.iterator.flat-map.js\");\n/* harmony import */ var core_js_modules_es_iterator_flat_map_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_flat_map_js__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! core-js/modules/es.iterator.for-each.js */ \"./node_modules/core-js/modules/es.iterator.for-each.js\");\n/* harmony import */ var core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_4__);\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! core-js/modules/es.iterator.map.js */ \"./node_modules/core-js/modules/es.iterator.map.js\");\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var core_js_modules_es_iterator_some_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! core-js/modules/es.iterator.some.js */ \"./node_modules/core-js/modules/es.iterator.some.js\");\n/* harmony import */ var core_js_modules_es_iterator_some_js__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_some_js__WEBPACK_IMPORTED_MODULE_6__);\n/* harmony import */ var core_js_modules_es_map_get_or_insert_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! core-js/modules/es.map.get-or-insert.js */ \"./node_modules/core-js/modules/es.map.get-or-insert.js\");\n/* harmony import */ var core_js_modules_es_map_get_or_insert_js__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_map_get_or_insert_js__WEBPACK_IMPORTED_MODULE_7__);\n/* harmony import */ var core_js_modules_es_map_get_or_insert_computed_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! core-js/modules/es.map.get-or-insert-computed.js */ \"./node_modules/core-js/modules/es.map.get-or-insert-computed.js\");\n/* harmony import */ var core_js_modules_es_map_get_or_insert_computed_js__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_map_get_or_insert_computed_js__WEBPACK_IMPORTED_MODULE_8__);\n/* harmony import */ var core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! core-js/modules/es.set.difference.v2.js */ \"./node_modules/core-js/modules/es.set.difference.v2.js\");\n/* harmony import */ var core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_9___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_9__);\n/* harmony import */ var core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! core-js/modules/es.set.intersection.v2.js */ \"./node_modules/core-js/modules/es.set.intersection.v2.js\");\n/* harmony import */ var core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_10___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_10__);\n/* harmony import */ var core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! core-js/modules/es.set.is-disjoint-from.v2.js */ \"./node_modules/core-js/modules/es.set.is-disjoint-from.v2.js\");\n/* harmony import */ var core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_11___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_11__);\n/* harmony import */ var core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! core-js/modules/es.set.is-subset-of.v2.js */ \"./node_modules/core-js/modules/es.set.is-subset-of.v2.js\");\n/* harmony import */ var core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_12___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_12__);\n/* harmony import */ var core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! core-js/modules/es.set.is-superset-of.v2.js */ \"./node_modules/core-js/modules/es.set.is-superset-of.v2.js\");\n/* harmony import */ var core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_13___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_13__);\n/* harmony import */ var core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(/*! core-js/modules/es.set.symmetric-difference.v2.js */ \"./node_modules/core-js/modules/es.set.symmetric-difference.v2.js\");\n/* harmony import */ var core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_14___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_14__);\n/* harmony import */ var core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(/*! core-js/modules/es.set.union.v2.js */ \"./node_modules/core-js/modules/es.set.union.v2.js\");\n/* harmony import */ var core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_15___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_15__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n/* harmony import */ var _stores_conversations__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(/*! @/stores/conversations */ \"./src/stores/conversations.js\");\n/* harmony import */ var _stores_contacts__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(/*! @/stores/contacts */ \"./src/stores/contacts.js\");\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n __name: 'ConversationList',\n setup(__props, {\n expose: __expose\n }) {\n __expose();\n const conversations = (0,_stores_conversations__WEBPACK_IMPORTED_MODULE_17__.useConversationStore)();\n const contacts = (0,_stores_contacts__WEBPACK_IMPORTED_MODULE_18__.useContactStore)();\n const actionMenuOpen = (0,vue__WEBPACK_IMPORTED_MODULE_16__.ref)(false);\n const activeAction = (0,vue__WEBPACK_IMPORTED_MODULE_16__.ref)('');\n const actionBusy = (0,vue__WEBPACK_IMPORTED_MODULE_16__.ref)(false);\n const searchKeyword = (0,vue__WEBPACK_IMPORTED_MODULE_16__.ref)('');\n const addKeyword = (0,vue__WEBPACK_IMPORTED_MODULE_16__.ref)('');\n const groupName = (0,vue__WEBPACK_IMPORTED_MODULE_16__.ref)('');\n const groupAnnouncement = (0,vue__WEBPACK_IMPORTED_MODULE_16__.ref)('');\n const groupWelcomeText = (0,vue__WEBPACK_IMPORTED_MODULE_16__.ref)('');\n const joinGroupKey = (0,vue__WEBPACK_IMPORTED_MODULE_16__.ref)('');\n const broadcastText = (0,vue__WEBPACK_IMPORTED_MODULE_16__.ref)('');\n const channelName = (0,vue__WEBPACK_IMPORTED_MODULE_16__.ref)('');\n const channelAnnouncement = (0,vue__WEBPACK_IMPORTED_MODULE_16__.ref)('');\n const contactKeyword = (0,vue__WEBPACK_IMPORTED_MODULE_16__.ref)('');\n const selectedContactIds = (0,vue__WEBPACK_IMPORTED_MODULE_16__.ref)([]);\n const actionTitles = {\n addContact: '添加联系人',\n createGroup: '新建群组',\n joinGroup: '加入群组',\n broadcast: '群发助手',\n createChannel: '创建频道/公告群'\n };\n const submitLabels = {\n addContact: '添加',\n createGroup: '创建',\n joinGroup: '加入',\n broadcast: '发送',\n createChannel: '创建'\n };\n const actionTitle = (0,vue__WEBPACK_IMPORTED_MODULE_16__.computed)(() => actionTitles[activeAction.value] || '');\n const submitLabel = (0,vue__WEBPACK_IMPORTED_MODULE_16__.computed)(() => submitLabels[activeAction.value] || '确定');\n const modalError = (0,vue__WEBPACK_IMPORTED_MODULE_16__.computed)(() => contacts.actionError || conversations.actionError);\n const modalInfo = (0,vue__WEBPACK_IMPORTED_MODULE_16__.computed)(() => contacts.actionInfo || conversations.actionNotice);\n const normalizedSearchKeyword = (0,vue__WEBPACK_IMPORTED_MODULE_16__.computed)(() => searchKeyword.value.trim().toLowerCase());\n const contactsByUid = (0,vue__WEBPACK_IMPORTED_MODULE_16__.computed)(() => {\n const map = new Map();\n contacts.contacts.forEach(contact => {\n const uid = String(contact.uid || contact.id || '');\n if (uid) {\n map.set(uid, contact);\n }\n });\n return map;\n });\n const filteredConversationItems = (0,vue__WEBPACK_IMPORTED_MODULE_16__.computed)(() => {\n const keyword = normalizedSearchKeyword.value;\n if (!keyword) {\n return conversations.conversations.map(item => conversationResult(item));\n }\n const conversationResults = conversations.conversations.filter(item => matchesConversation(item, keyword, contactForConversation(item))).map(item => conversationResult(item));\n const conversationTargets = new Set(conversations.conversations.filter(item => item.type === 'single').map(item => conversationTarget(item)));\n const contactResults = contacts.contacts.filter(item => !conversationTargets.has(String(item.uid || item.id || ''))).filter(item => matchesContact(item, keyword)).map(item => contactResult(item));\n return [...conversationResults, ...contactResults];\n });\n const filteredContacts = (0,vue__WEBPACK_IMPORTED_MODULE_16__.computed)(() => {\n const keyword = contactKeyword.value.toLowerCase();\n if (!keyword) {\n return contacts.contacts;\n }\n return contacts.contacts.filter(item => {\n return String(item.title || '').toLowerCase().includes(keyword) || String(item.subtitle || '').toLowerCase().includes(keyword) || String(item.uid || '').toLowerCase().includes(keyword);\n });\n });\n const submitDisabled = (0,vue__WEBPACK_IMPORTED_MODULE_16__.computed)(() => {\n if (activeAction.value === 'addContact') {\n return !addKeyword.value;\n }\n if (activeAction.value === 'createGroup') {\n return !groupName.value || !selectedContactIds.value.length;\n }\n if (activeAction.value === 'joinGroup') {\n return !joinGroupKey.value;\n }\n if (activeAction.value === 'broadcast') {\n return !broadcastText.value || !selectedContactIds.value.length;\n }\n if (activeAction.value === 'createChannel') {\n return !channelName.value;\n }\n return true;\n });\n (0,vue__WEBPACK_IMPORTED_MODULE_16__.onMounted)(() => {\n contacts.refreshContacts().catch(() => {});\n });\n const ContactPicker = (0,vue__WEBPACK_IMPORTED_MODULE_16__.defineComponent)({\n name: 'ContactPicker',\n props: {\n contacts: {\n type: Array,\n default: () => []\n },\n selectedIds: {\n type: Array,\n default: () => []\n },\n keyword: {\n type: String,\n default: ''\n },\n emptyText: {\n type: String,\n default: '暂无联系人'\n }\n },\n emits: ['toggle', 'update:keyword'],\n setup(props, {\n emit\n }) {\n return () => (0,vue__WEBPACK_IMPORTED_MODULE_16__.h)('div', {\n class: 'contact-picker'\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_16__.h)('input', {\n value: props.keyword,\n placeholder: '搜索联系人',\n onInput: event => emit('update:keyword', event.target.value)\n }), (0,vue__WEBPACK_IMPORTED_MODULE_16__.h)('div', {\n class: 'contact-picker-list'\n }, props.contacts.length ? props.contacts.map(contact => (0,vue__WEBPACK_IMPORTED_MODULE_16__.h)('button', {\n key: contact.uid,\n type: 'button',\n class: ['contact-picker-row', {\n selected: props.selectedIds.includes(contact.uid)\n }],\n onClick: () => emit('toggle', contact.uid)\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_16__.h)('span', {\n class: 'contact-check'\n }, props.selectedIds.includes(contact.uid) ? '✓' : ''), contact.portrait ? (0,vue__WEBPACK_IMPORTED_MODULE_16__.h)('img', {\n class: 'conversation-avatar image-avatar',\n src: contact.portrait,\n alt: ''\n }) : (0,vue__WEBPACK_IMPORTED_MODULE_16__.h)('span', {\n class: 'conversation-avatar'\n }, contact.avatarText), (0,vue__WEBPACK_IMPORTED_MODULE_16__.h)('span', {\n class: 'contact-picker-copy'\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_16__.h)('strong', contact.title), (0,vue__WEBPACK_IMPORTED_MODULE_16__.h)('small', contact.subtitle)])])) : [(0,vue__WEBPACK_IMPORTED_MODULE_16__.h)('div', {\n class: 'contact-picker-empty'\n }, props.emptyText)])]);\n }\n });\n function searchableValues(...values) {\n return values.flatMap(value => {\n if (Array.isArray(value)) {\n return value;\n }\n if (value && typeof value === 'object') {\n return Object.values(value);\n }\n return [value];\n }).map(value => String(value || '').toLowerCase()).filter(Boolean);\n }\n function matchesKeyword(values, keyword) {\n const compactKeyword = keyword.replace(/[^\\p{L}\\p{N}]+/gu, '');\n return searchableValues(values).some(value => {\n if (value.includes(keyword)) {\n return true;\n }\n if (!compactKeyword) {\n return false;\n }\n return value.replace(/[^\\p{L}\\p{N}]+/gu, '').includes(compactKeyword);\n });\n }\n function conversationTarget(item) {\n if (item.type !== 'single') {\n return String(item.target || item.raw?.target || item.id || '');\n }\n return String(item.target || item.raw?.target || item.id.replace(/^single-/, '') || '');\n }\n function contactForConversation(item) {\n const target = conversationTarget(item);\n return target ? contactsByUid.value.get(target) || null : null;\n }\n function matchesConversation(item, keyword, contact = null) {\n return matchesKeyword([item.title, item.subtitle, item.target, item.id, item.type, item.raw?.target, item.raw?.groupId, item.raw?.group_id, item.raw?.owner_uid, item.raw?.name, item.raw?.displayName, item.raw?.mobile, item.raw?.friendAlias, item.raw?.alias, contact?.title, contact?.subtitle, contact?.uid, contact?.id, contact?.alias, contact?.address, contact?.rawUser, contact?.rawFriend], keyword);\n }\n function matchesContact(item, keyword) {\n return matchesKeyword([item.title, item.subtitle, item.uid, item.id, item.alias, item.address, item.rawUser, item.rawFriend], keyword);\n }\n function conversationResult(item) {\n return {\n ...item,\n resultKind: 'conversation',\n resultKey: `conversation-${item.id}`\n };\n }\n function contactResult(contact) {\n return {\n id: contact.id,\n resultKind: 'contact',\n resultKey: `contact-${contact.uid || contact.id}`,\n contact,\n title: contact.title,\n subtitle: contact.subtitle,\n portrait: contact.portrait,\n avatarText: contact.avatarText,\n unread: 0\n };\n }\n function isListItemActive(item) {\n if (item.resultKind === 'conversation') {\n return item.id === conversations.activeId;\n }\n return `single-${item.contact?.uid || item.id}` === conversations.activeId;\n }\n function selectListItem(item) {\n if (item.resultKind === 'contact') {\n contacts.openChat(item.contact);\n return;\n }\n conversations.setActiveConversation(item.id);\n }\n function clearStoreMessages() {\n contacts.actionError = '';\n contacts.actionInfo = '';\n conversations.actionError = '';\n conversations.actionNotice = '';\n }\n function resetActionFields() {\n addKeyword.value = '';\n groupName.value = '';\n groupAnnouncement.value = '';\n groupWelcomeText.value = '';\n joinGroupKey.value = '';\n broadcastText.value = '';\n channelName.value = '';\n channelAnnouncement.value = '';\n contactKeyword.value = '';\n selectedContactIds.value = [];\n clearStoreMessages();\n }\n function toggleActionMenu() {\n actionMenuOpen.value = !actionMenuOpen.value;\n }\n function closeActionMenu() {\n actionMenuOpen.value = false;\n }\n function openAction(action) {\n resetActionFields();\n activeAction.value = action;\n actionMenuOpen.value = false;\n if (['createGroup', 'broadcast', 'createChannel'].includes(action)) {\n contacts.refreshContacts({\n force: true\n });\n }\n }\n function closeActionModal() {\n activeAction.value = '';\n actionBusy.value = false;\n resetActionFields();\n }\n function toggleContact(contactId) {\n const selected = new Set(selectedContactIds.value);\n if (selected.has(contactId)) {\n selected.delete(contactId);\n } else {\n selected.add(contactId);\n }\n selectedContactIds.value = Array.from(selected);\n }\n async function submitAction() {\n if (submitDisabled.value || actionBusy.value) {\n return;\n }\n actionBusy.value = true;\n clearStoreMessages();\n try {\n let ok = false;\n if (activeAction.value === 'addContact') {\n ok = await contacts.addContact(addKeyword.value);\n if (ok && contacts.activeContact) {\n contacts.openChat(contacts.activeContact);\n }\n } else if (activeAction.value === 'createGroup') {\n ok = await conversations.createGroup({\n name: groupName.value,\n announcement: groupAnnouncement.value,\n welcomeText: groupWelcomeText.value,\n memberIds: selectedContactIds.value\n });\n } else if (activeAction.value === 'joinGroup') {\n ok = await conversations.joinGroup(joinGroupKey.value);\n } else if (activeAction.value === 'broadcast') {\n ok = await conversations.broadcastMessage(selectedContactIds.value, broadcastText.value);\n } else if (activeAction.value === 'createChannel') {\n ok = await conversations.createGroup({\n name: channelName.value,\n memberIds: selectedContactIds.value,\n announcement: channelAnnouncement.value,\n channel: true\n });\n }\n if (ok) {\n window.setTimeout(closeActionModal, 450);\n }\n } finally {\n actionBusy.value = false;\n }\n }\n document.addEventListener('click', closeActionMenu);\n (0,vue__WEBPACK_IMPORTED_MODULE_16__.onBeforeUnmount)(() => {\n document.removeEventListener('click', closeActionMenu);\n });\n function formatUnread(value) {\n const count = Number(value || 0);\n return count > 99 ? '99+' : String(count);\n }\n const __returned__ = {\n conversations,\n contacts,\n actionMenuOpen,\n activeAction,\n actionBusy,\n searchKeyword,\n addKeyword,\n groupName,\n groupAnnouncement,\n groupWelcomeText,\n joinGroupKey,\n broadcastText,\n channelName,\n channelAnnouncement,\n contactKeyword,\n selectedContactIds,\n actionTitles,\n submitLabels,\n actionTitle,\n submitLabel,\n modalError,\n modalInfo,\n normalizedSearchKeyword,\n contactsByUid,\n filteredConversationItems,\n filteredContacts,\n submitDisabled,\n ContactPicker,\n searchableValues,\n matchesKeyword,\n conversationTarget,\n contactForConversation,\n matchesConversation,\n matchesContact,\n conversationResult,\n contactResult,\n isListItemActive,\n selectListItem,\n clearStoreMessages,\n resetActionFields,\n toggleActionMenu,\n closeActionMenu,\n openAction,\n closeActionModal,\n toggleContact,\n submitAction,\n formatUnread,\n computed: vue__WEBPACK_IMPORTED_MODULE_16__.computed,\n defineComponent: vue__WEBPACK_IMPORTED_MODULE_16__.defineComponent,\n h: vue__WEBPACK_IMPORTED_MODULE_16__.h,\n onBeforeUnmount: vue__WEBPACK_IMPORTED_MODULE_16__.onBeforeUnmount,\n onMounted: vue__WEBPACK_IMPORTED_MODULE_16__.onMounted,\n ref: vue__WEBPACK_IMPORTED_MODULE_16__.ref,\n get useConversationStore() {\n return _stores_conversations__WEBPACK_IMPORTED_MODULE_17__.useConversationStore;\n },\n get useContactStore() {\n return _stores_contacts__WEBPACK_IMPORTED_MODULE_18__.useContactStore;\n }\n };\n Object.defineProperty(__returned__, '__isScriptSetup', {\n enumerable: false,\n value: true\n });\n return __returned__;\n }\n});\n\n//# sourceURL=webpack://jc-web-im/./src/components/sidebar/ConversationList.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/NavigationRail.vue?vue&type=script&setup=true&lang=js": +/*!*******************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/NavigationRail.vue?vue&type=script&setup=true&lang=js ***! + \*******************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.iterator.constructor.js */ \"./node_modules/core-js/modules/es.iterator.constructor.js\");\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.iterator.find.js */ \"./node_modules/core-js/modules/es.iterator.find.js\");\n/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n/* harmony import */ var _stores_session__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @/stores/session */ \"./src/stores/session.js\");\n/* harmony import */ var _stores_navigation__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @/stores/navigation */ \"./src/stores/navigation.js\");\n/* harmony import */ var _stores_contacts__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @/stores/contacts */ \"./src/stores/contacts.js\");\n\n\n\n\n\n\nconst CROP_OUTPUT_SIZE = 512;\n/* harmony default export */ __webpack_exports__[\"default\"] = ({\n __name: 'NavigationRail',\n setup(__props, {\n expose: __expose\n }) {\n __expose();\n const session = (0,_stores_session__WEBPACK_IMPORTED_MODULE_3__.useSessionStore)();\n const navigation = (0,_stores_navigation__WEBPACK_IMPORTED_MODULE_4__.useNavigationStore)();\n const contacts = (0,_stores_contacts__WEBPACK_IMPORTED_MODULE_5__.useContactStore)();\n const settingsItems = [{\n key: 'notifications',\n label: '通知设置'\n }, {\n key: 'password',\n label: '更改密码'\n }, {\n key: 'wallet',\n label: '钱包管理'\n }, {\n key: 'language',\n label: '切换语言'\n }, {\n key: 'feedback',\n label: '问题反馈'\n }];\n const initials = (0,vue__WEBPACK_IMPORTED_MODULE_2__.computed)(() => (session.displayName || session.userId || 'Q').slice(0, 1).toUpperCase());\n const settingsOpen = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)(false);\n const settingsSection = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)('password');\n const passwordSaving = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)(false);\n const settingsNotice = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)('');\n const settingsError = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)('');\n const profileOpen = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)(false);\n const profileSaving = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)(false);\n const profileNotice = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)('');\n const profileError = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)('');\n const profileFile = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)(null);\n const profilePreview = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)('');\n const portraitInput = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)(null);\n const cropperCanvas = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)(null);\n const cropperPreviewCanvas = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)(null);\n const profileDraft = (0,vue__WEBPACK_IMPORTED_MODULE_2__.ref)({\n displayName: ''\n });\n const passwordDraft = (0,vue__WEBPACK_IMPORTED_MODULE_2__.reactive)({\n oldPassword: '',\n newPassword: '',\n confirmPassword: ''\n });\n const currentAvatarUrl = (0,vue__WEBPACK_IMPORTED_MODULE_2__.computed)(() => profilePreview.value || session.portrait || '');\n const activeSettingsItem = (0,vue__WEBPACK_IMPORTED_MODULE_2__.computed)(() => settingsItems.find(item => item.key === settingsSection.value) || settingsItems[0]);\n const avatarPreviewViewer = (0,vue__WEBPACK_IMPORTED_MODULE_2__.reactive)({\n open: false,\n url: '',\n scale: 1,\n offsetX: 0,\n offsetY: 0,\n dragging: false,\n dragStartX: 0,\n dragStartY: 0,\n dragOffsetX: 0,\n dragOffsetY: 0\n });\n const avatarCropper = (0,vue__WEBPACK_IMPORTED_MODULE_2__.reactive)({\n open: false,\n loaded: false,\n busy: false,\n error: '',\n objectUrl: '',\n fileName: 'avatar.jpg',\n image: null,\n baseScale: 1,\n scale: 1,\n rotation: 0,\n offsetX: 0,\n offsetY: 0,\n dragging: false,\n dragStartX: 0,\n dragStartY: 0,\n dragOffsetX: 0,\n dragOffsetY: 0\n });\n const avatarPreviewStyle = (0,vue__WEBPACK_IMPORTED_MODULE_2__.computed)(() => ({\n transform: `translate3d(${avatarPreviewViewer.offsetX}px, ${avatarPreviewViewer.offsetY}px, 0) scale(${avatarPreviewViewer.scale})`\n }));\n (0,vue__WEBPACK_IMPORTED_MODULE_2__.onMounted)(() => {\n session.refreshProfile();\n window.addEventListener('resize', drawAvatarCropper);\n });\n (0,vue__WEBPACK_IMPORTED_MODULE_2__.onBeforeUnmount)(() => {\n revokeProfilePreview();\n closeAvatarCropper();\n window.removeEventListener('resize', drawAvatarCropper);\n });\n function showContacts() {\n navigation.showContacts();\n contacts.refreshContacts();\n }\n function clearProfileFeedback() {\n profileNotice.value = '';\n profileError.value = '';\n }\n function clearSettingsFeedback() {\n settingsNotice.value = '';\n settingsError.value = '';\n }\n function resetPasswordDraft() {\n passwordDraft.oldPassword = '';\n passwordDraft.newPassword = '';\n passwordDraft.confirmPassword = '';\n }\n function openSettings() {\n settingsOpen.value = true;\n settingsSection.value = 'password';\n clearSettingsFeedback();\n }\n function closeSettings() {\n if (passwordSaving.value) {\n return;\n }\n settingsOpen.value = false;\n clearSettingsFeedback();\n resetPasswordDraft();\n }\n function selectSettingsSection(section) {\n settingsSection.value = section;\n clearSettingsFeedback();\n }\n function revokeProfilePreview() {\n if (profilePreview.value) {\n URL.revokeObjectURL(profilePreview.value);\n profilePreview.value = '';\n }\n }\n function revokeCropperObjectUrl() {\n if (avatarCropper.objectUrl) {\n URL.revokeObjectURL(avatarCropper.objectUrl);\n avatarCropper.objectUrl = '';\n }\n }\n async function openProfileEditor() {\n profileOpen.value = true;\n clearProfileFeedback();\n profileDraft.value.displayName = session.displayName || session.userId || '';\n const profile = await session.refreshProfile();\n if (profileOpen.value && profile) {\n profileDraft.value.displayName = session.displayName || session.userId || '';\n }\n }\n function closeProfileEditor() {\n profileOpen.value = false;\n profileFile.value = null;\n clearProfileFeedback();\n revokeProfilePreview();\n closeAvatarPreview();\n closeAvatarCropper();\n }\n function selectPortrait(event) {\n const file = event.target.files?.[0] || null;\n event.target.value = '';\n clearProfileFeedback();\n if (file) {\n openAvatarCropper(file);\n }\n }\n function choosePortraitFile() {\n portraitInput.value?.click();\n }\n function openAvatarPreview() {\n if (!currentAvatarUrl.value) {\n return;\n }\n avatarPreviewViewer.open = true;\n avatarPreviewViewer.url = currentAvatarUrl.value;\n resetAvatarPreview();\n }\n function closeAvatarPreview() {\n avatarPreviewViewer.open = false;\n avatarPreviewViewer.dragging = false;\n }\n function resetAvatarPreview() {\n avatarPreviewViewer.scale = 1;\n avatarPreviewViewer.offsetX = 0;\n avatarPreviewViewer.offsetY = 0;\n }\n function zoomAvatarPreview(factor) {\n avatarPreviewViewer.scale = Math.max(0.5, Math.min(6, Number((avatarPreviewViewer.scale * factor).toFixed(2))));\n if (avatarPreviewViewer.scale <= 1) {\n avatarPreviewViewer.offsetX = 0;\n avatarPreviewViewer.offsetY = 0;\n }\n }\n function handleAvatarPreviewWheel(event) {\n zoomAvatarPreview(event.deltaY < 0 ? 1.12 : 1 / 1.12);\n }\n function startAvatarPreviewDrag(event) {\n if (avatarPreviewViewer.scale <= 1) {\n return;\n }\n avatarPreviewViewer.dragging = true;\n avatarPreviewViewer.dragStartX = event.clientX;\n avatarPreviewViewer.dragStartY = event.clientY;\n avatarPreviewViewer.dragOffsetX = avatarPreviewViewer.offsetX;\n avatarPreviewViewer.dragOffsetY = avatarPreviewViewer.offsetY;\n event.currentTarget.setPointerCapture?.(event.pointerId);\n }\n function moveAvatarPreviewDrag(event) {\n if (!avatarPreviewViewer.dragging) {\n return;\n }\n avatarPreviewViewer.offsetX = avatarPreviewViewer.dragOffsetX + event.clientX - avatarPreviewViewer.dragStartX;\n avatarPreviewViewer.offsetY = avatarPreviewViewer.dragOffsetY + event.clientY - avatarPreviewViewer.dragStartY;\n }\n function stopAvatarPreviewDrag() {\n avatarPreviewViewer.dragging = false;\n }\n function dropPortrait(event) {\n const file = Array.from(event.dataTransfer?.files || []).find(item => item.type.startsWith('image/'));\n if (file) {\n clearProfileFeedback();\n openAvatarCropper(file);\n }\n }\n function cropperStageSize() {\n const canvas = cropperCanvas.value;\n const rect = canvas?.getBoundingClientRect();\n const width = Math.max(280, Math.round(rect?.width || 360));\n const height = Math.max(280, Math.round(rect?.height || 360));\n return {\n width,\n height\n };\n }\n function cropperBoxSize() {\n const {\n width,\n height\n } = cropperStageSize();\n return Math.round(Math.min(width, height) * 0.74);\n }\n function rotatedImageSize() {\n const image = avatarCropper.image;\n if (!image) {\n return {\n width: 1,\n height: 1\n };\n }\n const quarterTurn = Math.abs(avatarCropper.rotation % 180) === 90;\n return {\n width: quarterTurn ? image.naturalHeight : image.naturalWidth,\n height: quarterTurn ? image.naturalWidth : image.naturalHeight\n };\n }\n function clampCropperOffset() {\n const cropSize = cropperBoxSize();\n const rotated = rotatedImageSize();\n const scaledWidth = rotated.width * avatarCropper.scale;\n const scaledHeight = rotated.height * avatarCropper.scale;\n const maxX = Math.max(0, (scaledWidth - cropSize) / 2);\n const maxY = Math.max(0, (scaledHeight - cropSize) / 2);\n avatarCropper.offsetX = Math.max(-maxX, Math.min(maxX, avatarCropper.offsetX));\n avatarCropper.offsetY = Math.max(-maxY, Math.min(maxY, avatarCropper.offsetY));\n }\n function updateCropperBaseScale() {\n const cropSize = cropperBoxSize();\n const rotated = rotatedImageSize();\n avatarCropper.baseScale = Math.max(cropSize / rotated.width, cropSize / rotated.height);\n avatarCropper.scale = Math.max(avatarCropper.baseScale, avatarCropper.scale || avatarCropper.baseScale);\n }\n function drawCanvasImage(canvas, width, height, outputScale = 1, cropOnly = false) {\n const image = avatarCropper.image;\n if (!canvas || !image) {\n return;\n }\n const dpr = cropOnly ? 1 : window.devicePixelRatio || 1;\n canvas.width = Math.round(width * dpr);\n canvas.height = Math.round(height * dpr);\n const context = canvas.getContext('2d');\n context.setTransform(dpr, 0, 0, dpr, 0, 0);\n context.clearRect(0, 0, width, height);\n context.fillStyle = '#eef4f7';\n context.fillRect(0, 0, width, height);\n context.save();\n context.translate(width / 2 + avatarCropper.offsetX * outputScale, height / 2 + avatarCropper.offsetY * outputScale);\n context.rotate(avatarCropper.rotation * Math.PI / 180);\n context.scale(avatarCropper.scale * outputScale, avatarCropper.scale * outputScale);\n context.drawImage(image, -image.naturalWidth / 2, -image.naturalHeight / 2);\n context.restore();\n if (!cropOnly) {\n const cropSize = cropperBoxSize();\n context.save();\n context.fillStyle = 'rgba(10, 18, 30, 0.42)';\n context.beginPath();\n context.rect(0, 0, width, height);\n context.rect((width - cropSize) / 2, (height - cropSize) / 2, cropSize, cropSize);\n context.fill('evenodd');\n context.restore();\n }\n }\n function drawAvatarCropper() {\n if (!avatarCropper.open || !avatarCropper.loaded) {\n return;\n }\n clampCropperOffset();\n const {\n width,\n height\n } = cropperStageSize();\n drawCanvasImage(cropperCanvas.value, width, height);\n const previewCanvas = cropperPreviewCanvas.value;\n if (previewCanvas) {\n drawCanvasImage(previewCanvas, 108, 108, 108 / cropperBoxSize(), true);\n }\n }\n function resetCropperTransform() {\n if (!avatarCropper.image) {\n return;\n }\n avatarCropper.rotation = 0;\n avatarCropper.offsetX = 0;\n avatarCropper.offsetY = 0;\n updateCropperBaseScale();\n avatarCropper.scale = avatarCropper.baseScale;\n drawAvatarCropper();\n }\n function openAvatarCropper(file) {\n if (!file.type.startsWith('image/')) {\n profileError.value = '请选择图片文件';\n return;\n }\n if (file.size > 8 * 1024 * 1024) {\n profileError.value = '头像图片不能超过8MB';\n return;\n }\n closeAvatarCropper();\n avatarCropper.open = true;\n avatarCropper.loaded = false;\n avatarCropper.error = '';\n avatarCropper.fileName = file.name || 'avatar.jpg';\n avatarCropper.objectUrl = URL.createObjectURL(file);\n const image = new Image();\n image.onload = () => {\n avatarCropper.image = image;\n avatarCropper.loaded = true;\n (0,vue__WEBPACK_IMPORTED_MODULE_2__.nextTick)(resetCropperTransform);\n };\n image.onerror = () => {\n avatarCropper.error = '图片加载失败';\n avatarCropper.loaded = false;\n };\n image.src = avatarCropper.objectUrl;\n }\n function closeAvatarCropper() {\n avatarCropper.open = false;\n avatarCropper.loaded = false;\n avatarCropper.busy = false;\n avatarCropper.error = '';\n avatarCropper.image = null;\n avatarCropper.dragging = false;\n revokeCropperObjectUrl();\n }\n function zoomCropper(factor) {\n if (!avatarCropper.loaded) {\n return;\n }\n updateCropperBaseScale();\n avatarCropper.scale = Math.max(avatarCropper.baseScale, Math.min(avatarCropper.baseScale * 6, avatarCropper.scale * factor));\n drawAvatarCropper();\n }\n function rotateCropper(degrees) {\n if (!avatarCropper.loaded) {\n return;\n }\n avatarCropper.rotation = (avatarCropper.rotation + degrees + 360) % 360;\n updateCropperBaseScale();\n clampCropperOffset();\n drawAvatarCropper();\n }\n function handleCropperWheel(event) {\n zoomCropper(event.deltaY < 0 ? 1.08 : 1 / 1.08);\n }\n function startCropperDrag(event) {\n if (!avatarCropper.loaded) {\n return;\n }\n avatarCropper.dragging = true;\n avatarCropper.dragStartX = event.clientX;\n avatarCropper.dragStartY = event.clientY;\n avatarCropper.dragOffsetX = avatarCropper.offsetX;\n avatarCropper.dragOffsetY = avatarCropper.offsetY;\n event.currentTarget.setPointerCapture?.(event.pointerId);\n }\n function moveCropperDrag(event) {\n if (!avatarCropper.dragging) {\n return;\n }\n avatarCropper.offsetX = avatarCropper.dragOffsetX + event.clientX - avatarCropper.dragStartX;\n avatarCropper.offsetY = avatarCropper.dragOffsetY + event.clientY - avatarCropper.dragStartY;\n drawAvatarCropper();\n }\n function stopCropperDrag() {\n avatarCropper.dragging = false;\n }\n function canvasToBlob(canvas, type = 'image/jpeg', quality = 0.9) {\n return new Promise((resolve, reject) => {\n canvas.toBlob(blob => {\n if (blob) {\n resolve(blob);\n } else {\n reject(new Error('头像裁切失败'));\n }\n }, type, quality);\n });\n }\n async function confirmAvatarCrop() {\n if (!avatarCropper.loaded || avatarCropper.busy) {\n return;\n }\n avatarCropper.busy = true;\n avatarCropper.error = '';\n try {\n const outputCanvas = document.createElement('canvas');\n drawCanvasImage(outputCanvas, CROP_OUTPUT_SIZE, CROP_OUTPUT_SIZE, CROP_OUTPUT_SIZE / cropperBoxSize(), true);\n const blob = await canvasToBlob(outputCanvas);\n const fileName = avatarCropper.fileName.replace(/\\.[^.]+$/, '') || 'avatar';\n const croppedFile = new File([blob], `${fileName}.jpg`, {\n type: 'image/jpeg'\n });\n profileFile.value = croppedFile;\n revokeProfilePreview();\n profilePreview.value = URL.createObjectURL(croppedFile);\n closeAvatarCropper();\n } catch (error) {\n avatarCropper.error = error.message || '头像裁切失败';\n } finally {\n avatarCropper.busy = false;\n }\n }\n async function saveProfile() {\n if (profileSaving.value || !profileDraft.value.displayName) {\n return;\n }\n clearProfileFeedback();\n profileSaving.value = true;\n try {\n await session.saveProfile({\n displayName: profileDraft.value.displayName,\n portraitFile: profileFile.value\n });\n profileFile.value = null;\n revokeProfilePreview();\n profileNotice.value = '资料已保存';\n contacts.refreshContacts({\n force: true\n });\n } catch (error) {\n profileError.value = error.message || '资料保存失败';\n } finally {\n profileSaving.value = false;\n }\n }\n async function savePassword() {\n if (passwordSaving.value) {\n return;\n }\n clearSettingsFeedback();\n const oldPassword = passwordDraft.oldPassword;\n const newPassword = passwordDraft.newPassword;\n const confirmPassword = passwordDraft.confirmPassword;\n if (!oldPassword || !newPassword || !confirmPassword) {\n settingsError.value = '请填写完整密码';\n return;\n }\n if (newPassword !== confirmPassword) {\n settingsError.value = '两次输入的新密码不一致';\n return;\n }\n if (oldPassword === newPassword) {\n settingsError.value = '新密码不能和当前密码相同';\n return;\n }\n passwordSaving.value = true;\n try {\n await session.changePassword({\n oldPassword,\n newPassword\n });\n resetPasswordDraft();\n settingsNotice.value = '密码已更新';\n } catch (error) {\n settingsError.value = error.message || '密码修改失败';\n } finally {\n passwordSaving.value = false;\n }\n }\n function logout() {\n closeSettings();\n contacts.clearContacts();\n session.logout();\n }\n const __returned__ = {\n session,\n navigation,\n contacts,\n settingsItems,\n initials,\n settingsOpen,\n settingsSection,\n passwordSaving,\n settingsNotice,\n settingsError,\n profileOpen,\n profileSaving,\n profileNotice,\n profileError,\n profileFile,\n profilePreview,\n portraitInput,\n cropperCanvas,\n cropperPreviewCanvas,\n profileDraft,\n passwordDraft,\n currentAvatarUrl,\n activeSettingsItem,\n avatarPreviewViewer,\n avatarCropper,\n avatarPreviewStyle,\n CROP_OUTPUT_SIZE,\n showContacts,\n clearProfileFeedback,\n clearSettingsFeedback,\n resetPasswordDraft,\n openSettings,\n closeSettings,\n selectSettingsSection,\n revokeProfilePreview,\n revokeCropperObjectUrl,\n openProfileEditor,\n closeProfileEditor,\n selectPortrait,\n choosePortraitFile,\n openAvatarPreview,\n closeAvatarPreview,\n resetAvatarPreview,\n zoomAvatarPreview,\n handleAvatarPreviewWheel,\n startAvatarPreviewDrag,\n moveAvatarPreviewDrag,\n stopAvatarPreviewDrag,\n dropPortrait,\n cropperStageSize,\n cropperBoxSize,\n rotatedImageSize,\n clampCropperOffset,\n updateCropperBaseScale,\n drawCanvasImage,\n drawAvatarCropper,\n resetCropperTransform,\n openAvatarCropper,\n closeAvatarCropper,\n zoomCropper,\n rotateCropper,\n handleCropperWheel,\n startCropperDrag,\n moveCropperDrag,\n stopCropperDrag,\n canvasToBlob,\n confirmAvatarCrop,\n saveProfile,\n savePassword,\n logout,\n computed: vue__WEBPACK_IMPORTED_MODULE_2__.computed,\n nextTick: vue__WEBPACK_IMPORTED_MODULE_2__.nextTick,\n onBeforeUnmount: vue__WEBPACK_IMPORTED_MODULE_2__.onBeforeUnmount,\n onMounted: vue__WEBPACK_IMPORTED_MODULE_2__.onMounted,\n reactive: vue__WEBPACK_IMPORTED_MODULE_2__.reactive,\n ref: vue__WEBPACK_IMPORTED_MODULE_2__.ref,\n get useSessionStore() {\n return _stores_session__WEBPACK_IMPORTED_MODULE_3__.useSessionStore;\n },\n get useNavigationStore() {\n return _stores_navigation__WEBPACK_IMPORTED_MODULE_4__.useNavigationStore;\n },\n get useContactStore() {\n return _stores_contacts__WEBPACK_IMPORTED_MODULE_5__.useContactStore;\n }\n };\n Object.defineProperty(__returned__, '__isScriptSetup', {\n enumerable: false,\n value: true\n });\n return __returned__;\n }\n});\n\n//# sourceURL=webpack://jc-web-im/./src/components/sidebar/NavigationRail.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/App.vue?vue&type=template&id=7ba5bd90": +/*!******************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/App.vue?vue&type=template&id=7ba5bd90 ***! + \******************************************************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* binding */ render; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n return !$setup.session.isAuthenticated ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)($setup[\"LoginView\"], {\n key: 0\n })) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)($setup[\"ChatShell\"], {\n key: 1\n }));\n}\n\n//# sourceURL=webpack://jc-web-im/./src/App.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B3%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/ChatShell.vue?vue&type=template&id=2fe58db8": +/*!***********************************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/ChatShell.vue?vue&type=template&id=2fe58db8 ***! + \***********************************************************************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* binding */ render; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n\nconst _hoisted_1 = {\n class: \"app-shell\"\n};\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"main\", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)($setup[\"NavigationRail\"]), $setup.navigation.section === 'messages' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)($setup[\"ConversationList\"], {\n key: 0\n })) : $setup.navigation.section === 'favorites' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)($setup[\"FavoriteList\"], {\n key: 1\n })) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)($setup[\"ContactList\"], {\n key: 2\n })), $setup.navigation.section === 'messages' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)($setup[\"ChatPanel\"], {\n key: 3\n })) : $setup.navigation.section === 'favorites' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)($setup[\"FavoritePanel\"], {\n key: 4\n })) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)($setup[\"ContactPanel\"], {\n key: 5\n }))]);\n}\n\n//# sourceURL=webpack://jc-web-im/./src/components/ChatShell.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B3%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/LoginView.vue?vue&type=template&id=37959aa4": +/*!***********************************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/LoginView.vue?vue&type=template&id=37959aa4 ***! + \***********************************************************************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* binding */ render; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n\nconst _hoisted_1 = {\n class: \"login-page\"\n};\nconst _hoisted_2 = {\n class: \"login-panel\"\n};\nconst _hoisted_3 = {\n class: \"brand-lockup\"\n};\nconst _hoisted_4 = [\"src\", \"alt\"];\nconst _hoisted_5 = {\n class: \"login-tabs register-tabs\",\n style: {\n gridTemplateColumns: 'repeat(3, minmax(0, 1fr))'\n }\n};\nconst _hoisted_6 = [\"disabled\"];\nconst _hoisted_7 = [\"src\"];\nconst _hoisted_8 = {\n key: 1\n};\nconst _hoisted_9 = {\n key: 0,\n class: \"qr-scan-user\"\n};\nconst _hoisted_10 = [\"src\"];\nconst _hoisted_11 = {\n key: 1\n};\nconst _hoisted_12 = {\n class: \"qr-login-status\"\n};\nconst _hoisted_13 = [\"disabled\"];\nconst _hoisted_14 = [\"autocomplete\", \"inputmode\", \"placeholder\"];\nconst _hoisted_15 = [\"disabled\"];\nconst _hoisted_16 = {\n key: 2,\n class: \"form-error\"\n};\nconst _hoisted_17 = {\n key: 3,\n class: \"form-info\"\n};\nconst _hoisted_18 = {\n class: \"login-tabs register-tabs\",\n style: {\n gridTemplateColumns: 'repeat(2, minmax(0, 1fr))'\n }\n};\nconst _hoisted_19 = [\"autocomplete\", \"inputmode\", \"placeholder\"];\nconst _hoisted_20 = {\n key: 0\n};\nconst _hoisted_21 = {\n class: \"code-row\"\n};\nconst _hoisted_22 = [\"disabled\"];\nconst _hoisted_23 = [\"disabled\"];\nconst _hoisted_24 = {\n key: 1,\n class: \"form-error\"\n};\nconst _hoisted_25 = {\n key: 2,\n class: \"form-info\"\n};\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"main\", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"section\", _hoisted_2, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_3, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"img\", {\n class: \"brand-mark\",\n src: $setup.platformLogo,\n alt: `${$setup.platformName} logo`,\n onError: $setup.handleBrandLogoError\n }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_4), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"h1\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.platformName), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"

按照最新 App Server 文档接入的 Web IM 客户端

\")])]), $setup.view === 'login' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"form\", {\n key: 0,\n class: \"login-form\",\n onSubmit: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.submitLogin, [\"prevent\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_5, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)({\n active: $setup.loginMode === 'qr'\n }),\n onClick: _cache[0] || (_cache[0] = $event => $setup.setLoginMode('qr'))\n }, \"扫码登录\", 2 /* CLASS */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)({\n active: $setup.loginMode === 'phone'\n }),\n onClick: _cache[1] || (_cache[1] = $event => $setup.setLoginMode('phone'))\n }, \"手机号\", 2 /* CLASS */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)({\n active: $setup.loginMode === 'email'\n }),\n onClick: _cache[2] || (_cache[2] = $event => $setup.setLoginMode('email'))\n }, \"邮箱\", 2 /* CLASS */)]), $setup.loginMode === 'qr' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, {\n key: 0\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)([\"qr-login-card\", {\n scanned: $setup.qrStatus === 'scanned',\n expired: $setup.qrStatus === 'expired',\n error: $setup.qrStatus === 'error'\n }])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"qr-code-button\",\n disabled: $setup.qrStatus === 'loading' || $setup.session.connecting,\n onClick: $setup.refreshQrLogin\n }, [$setup.qrCodeImage ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"img\", {\n key: 0,\n src: $setup.qrCodeImage,\n alt: \"扫码登录二维码\"\n }, null, 8 /* PROPS */, _hoisted_7)) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_8, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.qrStatus === 'loading' ? '生成中' : '点击刷新'), 1 /* TEXT */))], 8 /* PROPS */, _hoisted_6), $setup.qrStatus === 'scanned' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_9, [$setup.qrUserPortrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"img\", {\n key: 0,\n src: $setup.qrUserPortrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_10)) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_11, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.qrUserInitial), 1 /* TEXT */))])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)], 2 /* CLASS */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_12, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.qrStatusTitle), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.qrStatusText), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"primary-action\",\n disabled: $setup.qrStatus === 'loading' || $setup.session.connecting,\n onClick: $setup.refreshQrLogin\n }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.qrRefreshText), 9 /* TEXT, PROPS */, _hoisted_13)], 64 /* STABLE_FRAGMENT */)) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, {\n key: 1\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.loginMode === 'phone' ? '手机号' : '邮箱'), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[3] || (_cache[3] = $event => $setup.loginAccount = $event),\n autocomplete: $setup.loginMode === 'phone' ? 'username' : 'email',\n inputmode: $setup.loginMode === 'phone' ? 'tel' : 'email',\n placeholder: $setup.loginMode === 'phone' ? '请输入手机号' : '请输入邮箱'\n }, null, 8 /* PROPS */, _hoisted_14), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.loginAccount, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[12] || (_cache[12] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"密码\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[4] || (_cache[4] = $event => $setup.loginPassword = $event),\n autocomplete: \"current-password\",\n type: \"password\",\n placeholder: \"请输入密码\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.loginPassword]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: \"primary-action\",\n disabled: $setup.session.connecting || !$setup.canSubmitLogin\n }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.session.connecting ? '登录中...' : '登录'), 9 /* TEXT, PROPS */, _hoisted_15)], 64 /* STABLE_FRAGMENT */)), $setup.session.error ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"p\", _hoisted_16, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.session.error), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), $setup.info ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"p\", _hoisted_17, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.info), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)], 32 /* NEED_HYDRATION */)) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"form\", {\n key: 1,\n class: \"login-form\",\n onSubmit: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.submitRegister, [\"prevent\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_18, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)({\n active: $setup.registerType === 1\n }),\n onClick: _cache[5] || (_cache[5] = $event => $setup.switchRegisterType(1))\n }, \"手机号注册\", 2 /* CLASS */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)({\n active: $setup.registerType === 2\n }),\n onClick: _cache[6] || (_cache[6] = $event => $setup.switchRegisterType(2))\n }, \"邮箱注册\", 2 /* CLASS */)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.registerType === 1 ? '手机号' : '邮箱'), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[7] || (_cache[7] = $event => $setup.registerForm.number = $event),\n autocomplete: $setup.registerType === 1 ? 'tel' : 'email',\n inputmode: $setup.registerType === 1 ? 'tel' : 'email',\n placeholder: $setup.registerType === 1 ? '请输入手机号' : '请输入邮箱'\n }, null, 8 /* PROPS */, _hoisted_19), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.registerForm.number, void 0, {\n trim: true\n }]])]), $setup.registerType === 2 ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"label\", _hoisted_20, [_cache[13] || (_cache[13] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"邮箱验证码\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_21, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[8] || (_cache[8] = $event => $setup.registerForm.code = $event),\n autocomplete: \"one-time-code\",\n inputmode: \"numeric\",\n placeholder: \"请输入邮箱验证码\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.registerForm.code, void 0, {\n trim: true\n }]]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"code-action\",\n disabled: $setup.sendingCode || !$setup.canRequestEmailCode,\n onClick: $setup.requestEmailCode\n }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.sendingCode ? '发送中' : '发送验证码'), 9 /* TEXT, PROPS */, _hoisted_22)])])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[14] || (_cache[14] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"密码\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[9] || (_cache[9] = $event => $setup.registerForm.password = $event),\n autocomplete: \"new-password\",\n type: \"password\",\n placeholder: \"请输入密码\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.registerForm.password]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[15] || (_cache[15] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"确认密码\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[10] || (_cache[10] = $event => $setup.registerForm.confirmPassword = $event),\n autocomplete: \"new-password\",\n type: \"password\",\n placeholder: \"请再次输入密码\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.registerForm.confirmPassword]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[16] || (_cache[16] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"邀请码\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[11] || (_cache[11] = $event => $setup.registerForm.invitationCode = $event),\n autocomplete: \"off\",\n placeholder: \"没有可留空\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.registerForm.invitationCode, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: \"primary-action\",\n disabled: $setup.session.connecting || !$setup.canSubmitRegister\n }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.session.connecting ? '注册中...' : '注册并登录'), 9 /* TEXT, PROPS */, _hoisted_23), $setup.session.error ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"p\", _hoisted_24, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.session.error), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), $setup.info ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"p\", _hoisted_25, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.info), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)], 32 /* NEED_HYDRATION */)), $setup.view === 'login' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"button\", {\n key: 2,\n class: \"ghost-action\",\n type: \"button\",\n onClick: $setup.openRegister\n }, \" 新用户注册 \")) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"button\", {\n key: 3,\n class: \"ghost-action\",\n type: \"button\",\n onClick: $setup.backToLogin\n }, \" 返回登录 \"))])]);\n}\n\n//# sourceURL=webpack://jc-web-im/./src/components/LoginView.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B3%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/ChatPanel.vue?vue&type=template&id=358a1705": +/*!****************************************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/ChatPanel.vue?vue&type=template&id=358a1705 ***! + \****************************************************************************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* binding */ render; }\n/* harmony export */ });\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.includes.js */ \"./node_modules/core-js/modules/es.array.includes.js\");\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n\n\nconst _hoisted_1 = {\n class: \"chat-panel\"\n};\nconst _hoisted_2 = {\n class: \"chat-header\"\n};\nconst _hoisted_3 = [\"src\"];\nconst _hoisted_4 = {\n key: 1,\n class: \"chat-title-avatar\"\n};\nconst _hoisted_5 = {\n class: \"chat-title-copy\"\n};\nconst _hoisted_6 = [\"src\"];\nconst _hoisted_7 = {\n key: 1,\n class: \"chat-title-avatar\"\n};\nconst _hoisted_8 = {\n class: \"chat-title-copy\"\n};\nconst _hoisted_9 = {\n key: 2\n};\nconst _hoisted_10 = {\n class: \"header-actions chat-header-actions\"\n};\nconst _hoisted_11 = {\n key: 0,\n class: \"empty-chat\"\n};\nconst _hoisted_12 = {\n key: 2,\n class: \"selection-toolbar\"\n};\nconst _hoisted_13 = {\n class: \"forward-dialog\"\n};\nconst _hoisted_14 = [\"onClick\"];\nconst _hoisted_15 = {\n class: \"conversation-avatar\"\n};\nconst _hoisted_16 = {\n type: \"submit\"\n};\nconst _hoisted_17 = {\n class: \"group-dialog message-detail-dialog\"\n};\nconst _hoisted_18 = {\n key: 0,\n class: \"group-loading\"\n};\nconst _hoisted_19 = {\n class: \"group-profile message-detail-meta\"\n};\nconst _hoisted_20 = {\n key: 0\n};\nconst _hoisted_21 = {\n key: 0,\n class: \"inline-save-status error\"\n};\nconst _hoisted_22 = {\n class: \"message-detail-summary\"\n};\nconst _hoisted_23 = {\n class: \"message-detail-groups\"\n};\nconst _hoisted_24 = {\n class: \"message-detail-section\"\n};\nconst _hoisted_25 = {\n key: 0,\n class: \"message-detail-member-list\"\n};\nconst _hoisted_26 = [\"src\"];\nconst _hoisted_27 = {\n key: 1,\n class: \"conversation-avatar\"\n};\nconst _hoisted_28 = {\n class: \"message-detail-member-main\"\n};\nconst _hoisted_29 = {\n class: \"message-detail-member-time\"\n};\nconst _hoisted_30 = {\n key: 2,\n class: \"message-detail-role\"\n};\nconst _hoisted_31 = {\n key: 1,\n class: \"contact-picker-empty\"\n};\nconst _hoisted_32 = {\n class: \"message-detail-section\"\n};\nconst _hoisted_33 = {\n key: 0,\n class: \"message-detail-member-list\"\n};\nconst _hoisted_34 = [\"src\"];\nconst _hoisted_35 = {\n key: 1,\n class: \"conversation-avatar\"\n};\nconst _hoisted_36 = {\n class: \"message-detail-member-main\"\n};\nconst _hoisted_37 = {\n class: \"message-detail-member-time\"\n};\nconst _hoisted_38 = {\n key: 2,\n class: \"message-detail-role\"\n};\nconst _hoisted_39 = {\n key: 1,\n class: \"contact-picker-empty\"\n};\nconst _hoisted_40 = {\n class: \"message-detail-section\"\n};\nconst _hoisted_41 = {\n key: 0,\n class: \"message-detail-member-list\"\n};\nconst _hoisted_42 = [\"src\"];\nconst _hoisted_43 = {\n key: 1,\n class: \"conversation-avatar\"\n};\nconst _hoisted_44 = {\n class: \"message-detail-member-main\"\n};\nconst _hoisted_45 = {\n key: 2,\n class: \"message-detail-role\"\n};\nconst _hoisted_46 = {\n key: 1,\n class: \"contact-picker-empty\"\n};\nconst _hoisted_47 = {\n class: \"group-dialog single-profile-dialog\"\n};\nconst _hoisted_48 = {\n class: \"single-profile-summary\"\n};\nconst _hoisted_49 = [\"src\"];\nconst _hoisted_50 = {\n key: 1,\n class: \"profile-avatar\"\n};\nconst _hoisted_51 = {\n class: \"group-profile\"\n};\nconst _hoisted_52 = {\n class: \"single-profile-footer\"\n};\nconst _hoisted_53 = {\n key: 0,\n class: \"inline-save-status\"\n};\nconst _hoisted_54 = {\n key: 1,\n class: \"inline-save-status error\"\n};\nconst _hoisted_55 = [\"disabled\"];\nconst _hoisted_56 = [\"disabled\"];\nconst _hoisted_57 = {\n key: 0,\n class: \"group-dialog\"\n};\nconst _hoisted_58 = {\n key: 0,\n class: \"group-loading\"\n};\nconst _hoisted_59 = {\n key: 1,\n class: \"group-profile\"\n};\nconst _hoisted_60 = [\"disabled\"];\nconst _hoisted_61 = [\"disabled\"];\nconst _hoisted_62 = [\"disabled\"];\nconst _hoisted_63 = {\n key: 1,\n class: \"group-dialog\"\n};\nconst _hoisted_64 = {\n key: 0,\n class: \"group-loading\"\n};\nconst _hoisted_65 = {\n key: 1,\n class: \"group-member-list\"\n};\nconst _hoisted_66 = [\"src\"];\nconst _hoisted_67 = {\n key: 1,\n class: \"conversation-avatar\"\n};\nconst _hoisted_68 = {\n key: 0,\n class: \"contact-picker-empty\"\n};\nconst _hoisted_69 = [\"disabled\"];\nconst _hoisted_70 = {\n key: 3,\n class: \"group-dialog\"\n};\nconst _hoisted_71 = {\n key: 0,\n class: \"group-loading\"\n};\nconst _hoisted_72 = {\n key: 1,\n class: \"group-member-list\"\n};\nconst _hoisted_73 = [\"onClick\"];\nconst _hoisted_74 = [\"src\"];\nconst _hoisted_75 = {\n key: 1,\n class: \"conversation-avatar\"\n};\nconst _hoisted_76 = {\n key: 0,\n class: \"contact-picker-empty\"\n};\nconst _hoisted_77 = [\"disabled\"];\nconst _hoisted_78 = {\n key: 4,\n class: \"group-dialog group-management-dialog\"\n};\nconst _hoisted_79 = {\n class: \"group-management-tabs\"\n};\nconst _hoisted_80 = {\n key: 0,\n class: \"group-loading\"\n};\nconst _hoisted_81 = {\n key: 1,\n class: \"group-manage-section\"\n};\nconst _hoisted_82 = {\n class: \"group-member-list\"\n};\nconst _hoisted_83 = [\"checked\", \"onChange\"];\nconst _hoisted_84 = [\"src\"];\nconst _hoisted_85 = {\n key: 1,\n class: \"conversation-avatar\"\n};\nconst _hoisted_86 = {\n key: 0,\n class: \"contact-picker-empty\"\n};\nconst _hoisted_87 = {\n key: 2,\n class: \"group-manage-section\"\n};\nconst _hoisted_88 = {\n key: 3,\n class: \"group-manage-section\"\n};\nconst _hoisted_89 = {\n class: \"group-member-list\"\n};\nconst _hoisted_90 = [\"checked\", \"onChange\"];\nconst _hoisted_91 = [\"src\"];\nconst _hoisted_92 = {\n key: 1,\n class: \"conversation-avatar\"\n};\nconst _hoisted_93 = {\n key: 0,\n class: \"contact-picker-empty\"\n};\nconst _hoisted_94 = [\"disabled\"];\nconst _hoisted_95 = {\n key: 4,\n class: \"group-manage-section\"\n};\nconst _hoisted_96 = {\n key: 0,\n class: \"mute-time-grid\"\n};\nconst _hoisted_97 = {\n key: 1,\n class: \"mute-time-grid\"\n};\nconst _hoisted_98 = {\n class: \"mute-save-footer\"\n};\nconst _hoisted_99 = {\n key: 0,\n class: \"inline-save-status\"\n};\nconst _hoisted_100 = {\n key: 1,\n class: \"inline-save-status error\"\n};\nconst _hoisted_101 = [\"disabled\"];\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n return (0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"section\", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"header\", _hoisted_2, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", null, [$setup.isGroupConversation ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"button\", {\n key: 0,\n type: \"button\",\n class: \"chat-title-button\",\n onClick: $setup.openGroupOverview\n }, [$setup.activeConversation?.portrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"img\", {\n key: 0,\n class: \"chat-title-avatar image-avatar\",\n src: $setup.activeConversation.portrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_3)) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_4, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.activeConversation?.avatarText || '?'), 1 /* TEXT */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", _hoisted_5, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"h2\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.activeConversation?.title || '未选择会话'), 1 /* TEXT */)])])) : $setup.activeConversation ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"button\", {\n key: 1,\n type: \"button\",\n class: \"chat-title-button\",\n onClick: $setup.openSingleProfile\n }, [$setup.singleProfile.portrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"img\", {\n key: 0,\n class: \"chat-title-avatar image-avatar\",\n src: $setup.singleProfile.portrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_6)) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_7, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.singleProfile.avatarText), 1 /* TEXT */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", _hoisted_8, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"h2\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.activeConversation?.title || '未选择会话'), 1 /* TEXT */)])])) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"h2\", _hoisted_9, \"未选择会话\")), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"p\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.statusText), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", _hoisted_10, [_cache[29] || (_cache[29] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n class: \"icon-button\",\n title: \"搜索\"\n }, \"⌕\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n class: \"icon-button\",\n title: \"更多\",\n onClick: (0,vue__WEBPACK_IMPORTED_MODULE_1__.withModifiers)($setup.toggleGroupMenu, [\"stop\"])\n }, \"⋯\"), $setup.groupMenuOpen && $setup.isGroupConversation ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", {\n key: 0,\n class: \"quick-action-menu chat-more-menu\",\n onClick: _cache[0] || (_cache[0] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.withModifiers)(() => {}, [\"stop\"]))\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n onClick: $setup.openGroupMembers\n }, \"查看群员\"), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n onClick: $setup.openGroupAlias\n }, \"我在本群的昵称\"), $setup.canManageGroup ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"button\", {\n key: 0,\n type: \"button\",\n onClick: $setup.openGroupManage\n }, \"群组管理\")) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true)])) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true)])]), !$setup.activeConversation ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_11, _cache[30] || (_cache[30] = [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"strong\", null, \"选择一个会话\", -1 /* HOISTED */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, \"真实会话同步完成后,可以在左侧打开聊天\", -1 /* HOISTED */)]))) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createBlock)($setup[\"MessageList\"], {\n key: 1,\n messages: $setup.conversations.activeMessages,\n \"selection-mode\": $setup.conversations.selectionMode,\n \"selected-message-ids\": $setup.conversations.selectedMessageIds,\n \"favorite-message-ids\": $setup.conversations.favoriteMessageIds,\n \"attention-message-id\": $setup.conversations.attentionMessageId,\n \"can-recall-others\": $setup.canManageGroup && $setup.isGroupConversation,\n onMessageAction: $setup.conversations.handleMessageAction,\n onToggleSelect: $setup.conversations.toggleSelectedMessage,\n onSenderProfileUpdated: $setup.handleSenderProfileUpdated\n }, null, 8 /* PROPS */, [\"messages\", \"selection-mode\", \"selected-message-ids\", \"favorite-message-ids\", \"attention-message-id\", \"can-recall-others\", \"onMessageAction\", \"onToggleSelect\"])), $setup.conversations.selectionMode ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_12, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, \"已选 \" + (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.conversations.selectedMessageIds.length) + \" 条\", 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n onClick: _cache[1] || (_cache[1] = (...args) => $setup.conversations.copySelectedMessages && $setup.conversations.copySelectedMessages(...args))\n }, \"复制\"), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n onClick: _cache[2] || (_cache[2] = (...args) => $setup.conversations.deleteSelectedMessages && $setup.conversations.deleteSelectedMessages(...args))\n }, \"删除\"), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n onClick: _cache[3] || (_cache[3] = (...args) => $setup.conversations.startForwardSelectedMessages && $setup.conversations.startForwardSelectedMessages(...args))\n }, \"转发\"), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n onClick: _cache[4] || (_cache[4] = (...args) => $setup.conversations.clearSelection && $setup.conversations.clearSelection(...args))\n }, \"取消\")])) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createVNode)($setup[\"MessageComposer\"], {\n disabled: !$setup.activeConversation || !$setup.session.isAuthenticated || !$setup.conversations.canSendActiveConversation,\n \"quote-message\": $setup.conversations.quotedMessage,\n onCancelQuote: $setup.conversations.cancelQuote,\n onSend: $setup.conversations.sendText,\n onSendAttachment: $setup.conversations.sendAttachment\n }, null, 8 /* PROPS */, [\"disabled\", \"quote-message\", \"onCancelQuote\", \"onSend\", \"onSendAttachment\"]), $setup.conversations.forwardingMessages.length ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", {\n key: 3,\n class: \"modal-backdrop\",\n onClick: _cache[6] || (_cache[6] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.withModifiers)((...args) => $setup.conversations.cancelForward && $setup.conversations.cancelForward(...args), [\"self\"]))\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"section\", _hoisted_13, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"header\", null, [_cache[31] || (_cache[31] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"h3\", null, \"转发给\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: _cache[5] || (_cache[5] = (...args) => $setup.conversations.cancelForward && $setup.conversations.cancelForward(...args))\n }, \"×\")]), ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_1__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.renderList)($setup.conversations.conversations, item => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"button\", {\n key: item.id,\n type: \"button\",\n class: \"forward-target\",\n onClick: $event => $setup.conversations.completeForward(item.id)\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", _hoisted_15, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)(item.avatarText), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)(item.title), 1 /* TEXT */)], 8 /* PROPS */, _hoisted_14);\n }), 128 /* KEYED_FRAGMENT */))])])) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true), $setup.conversations.editingMessage ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", {\n key: 4,\n class: \"modal-backdrop\",\n onClick: _cache[11] || (_cache[11] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.withModifiers)((...args) => $setup.conversations.cancelEditMessage && $setup.conversations.cancelEditMessage(...args), [\"self\"]))\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"form\", {\n class: \"edit-dialog\",\n onSubmit: _cache[10] || (_cache[10] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.withModifiers)((...args) => $setup.conversations.completeEditMessage && $setup.conversations.completeEditMessage(...args), [\"prevent\"]))\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"header\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"h3\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.conversations.editingMode === 'resend' ? '重新编辑' : '编辑消息'), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: _cache[7] || (_cache[7] = (...args) => $setup.conversations.cancelEditMessage && $setup.conversations.cancelEditMessage(...args))\n }, \"×\")]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"textarea\", {\n \"onUpdate:modelValue\": _cache[8] || (_cache[8] = $event => $setup.conversations.editingText = $event),\n maxlength: \"5000\",\n rows: \"5\",\n autofocus: \"\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_1__.vModelText, $setup.conversations.editingText]]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"footer\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n onClick: _cache[9] || (_cache[9] = (...args) => $setup.conversations.cancelEditMessage && $setup.conversations.cancelEditMessage(...args))\n }, \"取消\"), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", _hoisted_16, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.conversations.editingMode === 'resend' ? '发送' : '保存'), 1 /* TEXT */)])], 32 /* NEED_HYDRATION */)])) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true), $setup.conversations.messageDetailOpen ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", {\n key: 5,\n class: \"modal-backdrop\",\n onClick: _cache[13] || (_cache[13] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.withModifiers)((...args) => $setup.conversations.closeMessageDetail && $setup.conversations.closeMessageDetail(...args), [\"self\"]))\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"section\", _hoisted_17, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"header\", null, [_cache[32] || (_cache[32] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"h3\", null, \"消息详情\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: _cache[12] || (_cache[12] = (...args) => $setup.conversations.closeMessageDetail && $setup.conversations.closeMessageDetail(...args))\n }, \"×\")]), $setup.conversations.messageDetailLoading ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_18, \"正在加载\")) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_1__.Fragment, {\n key: 1\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dl\", _hoisted_19, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", null, [_cache[33] || (_cache[33] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dt\", null, \"发送时间\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.formatDetailTime($setup.messageDetail?.sentAt)), 1 /* TEXT */)]), $setup.messageDetail?.editedAt ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_20, [_cache[34] || (_cache[34] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dt\", null, \"编辑时间\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.formatDetailTime($setup.messageDetail.editedAt)), 1 /* TEXT */)])) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", null, [_cache[35] || (_cache[35] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dt\", null, \"发送人\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.detailMemberName($setup.messageDetail?.sender)), 1 /* TEXT */)]), !$setup.isMessageDetailGroup ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_1__.Fragment, {\n key: 1\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", null, [_cache[36] || (_cache[36] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dt\", null, \"接收人\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.detailMemberName($setup.messageDetail?.receiver)), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", null, [_cache[37] || (_cache[37] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dt\", null, \"送达时间\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dd\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_1__.normalizeClass)({\n 'message-detail-muted': !$setup.messageDetail?.deliveredAt\n })\n }, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.messageDetail?.deliveredAt ? $setup.formatDetailTime($setup.messageDetail.deliveredAt) : '未送达'), 3 /* TEXT, CLASS */)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", null, [_cache[38] || (_cache[38] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dt\", null, \"已读时间\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dd\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_1__.normalizeClass)({\n 'message-detail-muted': !$setup.messageDetail?.readAt\n })\n }, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.messageDetail?.readAt ? $setup.formatDetailTime($setup.messageDetail.readAt) : '未读'), 3 /* TEXT, CLASS */)])], 64 /* STABLE_FRAGMENT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true)]), $setup.conversations.messageDetailError ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_21, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.conversations.messageDetailError), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true), $setup.isMessageDetailGroup ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_1__.Fragment, {\n key: 1\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", _hoisted_22, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, [_cache[39] || (_cache[39] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createTextVNode)(\"已读 \")), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.messageDetailCounts.read || 0), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, [_cache[40] || (_cache[40] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createTextVNode)(\"未读 \")), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.messageDetailCounts.unread || 0), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, [_cache[41] || (_cache[41] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createTextVNode)(\"未送达 \")), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.messageDetailCounts.undelivered || 0), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, [_cache[42] || (_cache[42] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createTextVNode)(\"成员 \")), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.messageDetailCounts.total || 0), 1 /* TEXT */)])]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", _hoisted_23, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"section\", _hoisted_24, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"header\", null, [_cache[43] || (_cache[43] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"strong\", null, \"已读\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.messageDetailCounts.read || 0) + \" 人\", 1 /* TEXT */)]), $setup.messageDetailGroup?.read?.length ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_25, [((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_1__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.renderList)($setup.messageDetailGroup.read, member => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", {\n key: `read-${member.uid}`,\n class: \"message-detail-member-row\"\n }, [member.portrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"img\", {\n key: 0,\n class: \"conversation-avatar image-avatar\",\n src: member.portrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_26)) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_27, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.detailMemberAvatar(member)), 1 /* TEXT */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", _hoisted_28, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.detailMemberName(member)), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"small\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)(member.uid), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", _hoisted_29, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.formatDetailTime(member.time)), 1 /* TEXT */), $setup.detailRoleLabel(member.role) ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_30, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.detailRoleLabel(member.role)), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true)]);\n }), 128 /* KEYED_FRAGMENT */))])) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_31, \"暂无已读成员\"))]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"section\", _hoisted_32, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"header\", null, [_cache[44] || (_cache[44] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"strong\", null, \"未读\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.messageDetailCounts.unread || 0) + \" 人\", 1 /* TEXT */)]), $setup.messageDetailGroup?.unread?.length ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_33, [((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_1__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.renderList)($setup.messageDetailGroup.unread, member => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", {\n key: `unread-${member.uid}`,\n class: \"message-detail-member-row\"\n }, [member.portrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"img\", {\n key: 0,\n class: \"conversation-avatar image-avatar\",\n src: member.portrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_34)) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_35, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.detailMemberAvatar(member)), 1 /* TEXT */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", _hoisted_36, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.detailMemberName(member)), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"small\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)(member.uid), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", _hoisted_37, \"送达 \" + (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.formatDetailTime(member.time)), 1 /* TEXT */), $setup.detailRoleLabel(member.role) ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_38, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.detailRoleLabel(member.role)), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true)]);\n }), 128 /* KEYED_FRAGMENT */))])) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_39, \"暂无未读成员\"))]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"section\", _hoisted_40, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"header\", null, [_cache[45] || (_cache[45] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"strong\", null, \"未送达\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.messageDetailCounts.undelivered || 0) + \" 人\", 1 /* TEXT */)]), $setup.messageDetailGroup?.undelivered?.length ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_41, [((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_1__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.renderList)($setup.messageDetailGroup.undelivered, member => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", {\n key: `undelivered-${member.uid}`,\n class: \"message-detail-member-row\"\n }, [member.portrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"img\", {\n key: 0,\n class: \"conversation-avatar image-avatar\",\n src: member.portrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_42)) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_43, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.detailMemberAvatar(member)), 1 /* TEXT */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", _hoisted_44, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.detailMemberName(member)), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"small\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)(member.uid), 1 /* TEXT */)]), _cache[46] || (_cache[46] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", {\n class: \"message-detail-member-time message-detail-muted\"\n }, \"未送达\", -1 /* HOISTED */)), $setup.detailRoleLabel(member.role) ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_45, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.detailRoleLabel(member.role)), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true)]);\n }), 128 /* KEYED_FRAGMENT */))])) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_46, \"暂无未送达成员\"))])])], 64 /* STABLE_FRAGMENT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true)], 64 /* STABLE_FRAGMENT */))])])) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true), $setup.singleProfileOpen ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", {\n key: 6,\n class: \"modal-backdrop\",\n onClick: (0,vue__WEBPACK_IMPORTED_MODULE_1__.withModifiers)($setup.closeSingleProfile, [\"self\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"section\", _hoisted_47, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"header\", null, [_cache[47] || (_cache[47] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"h3\", null, \"联系人资料\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: $setup.closeSingleProfile\n }, \"×\")]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", _hoisted_48, [$setup.singleProfile.portrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"img\", {\n key: 0,\n class: \"profile-avatar image-avatar\",\n src: $setup.singleProfile.portrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_49)) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_50, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.singleProfile.avatarText), 1 /* TEXT */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.singleProfile.displayName), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dl\", _hoisted_51, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", null, [_cache[48] || (_cache[48] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dt\", null, \"用户ID\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.singleProfile.uid || '-'), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", null, [_cache[49] || (_cache[49] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dt\", null, \"昵称\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.singleProfile.rawName || '-'), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", null, [_cache[50] || (_cache[50] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dt\", null, \"手机号\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.singleProfile.mobile || '未知'), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", null, [_cache[51] || (_cache[51] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dt\", null, \"地区\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.singleProfile.address || '未知'), 1 /* TEXT */)])]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"label\", null, [_cache[52] || (_cache[52] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, \"备注名称\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[14] || (_cache[14] = $event => $setup.singleAliasDraft = $event),\n maxlength: \"32\",\n autocomplete: \"off\",\n placeholder: \"留空使用对方昵称\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_1__.vModelText, $setup.singleAliasDraft, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"footer\", _hoisted_52, [$setup.singleProfileNotice ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_53, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.singleProfileNotice), 1 /* TEXT */)) : $setup.singleProfileError ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_54, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.singleProfileError), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"ghost-inline-action\",\n disabled: $setup.singleProfileSaving,\n onClick: $setup.closeSingleProfile\n }, \"取消\", 8 /* PROPS */, _hoisted_55), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"primary-inline-action\",\n disabled: $setup.singleProfileSaving || !$setup.singleProfile.uid,\n onClick: $setup.saveSingleAlias\n }, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.singleProfileSaving ? '保存中...' : '保存备注'), 9 /* TEXT, PROPS */, _hoisted_56)])])])) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true), $setup.groupPanel ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", {\n key: 7,\n class: \"modal-backdrop\",\n onClick: (0,vue__WEBPACK_IMPORTED_MODULE_1__.withModifiers)($setup.closeGroupPanel, [\"self\"])\n }, [$setup.groupPanel === 'overview' ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"section\", _hoisted_57, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"header\", null, [_cache[53] || (_cache[53] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"h3\", null, \"群概况\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: $setup.closeGroupPanel\n }, \"×\")]), $setup.groupBusy ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_58, \"正在加载\")) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"dl\", _hoisted_59, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", null, [_cache[54] || (_cache[54] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dt\", null, \"群名\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.groupDetail?.name || $setup.activeConversation?.title || '-'), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", null, [_cache[55] || (_cache[55] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dt\", null, \"群ID\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.groupDetail?.groupId || $setup.activeConversation?.target || '-'), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", null, [_cache[56] || (_cache[56] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dt\", null, \"群说明\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.groupDetail?.announcement || '未设置'), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", null, [_cache[57] || (_cache[57] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dt\", null, \"群主昵称\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.groupDetail?.ownerName || '-'), 1 /* TEXT */)])])), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"footer\", null, [$setup.isGroupOwner ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_1__.Fragment, {\n key: 0\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"danger-inline-action\",\n disabled: $setup.groupBusy,\n onClick: $setup.dismissCurrentGroup\n }, \"解散群组\", 8 /* PROPS */, _hoisted_60), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"primary-inline-action\",\n disabled: $setup.groupBusy,\n onClick: $setup.openTransferGroup\n }, \"转让群组\", 8 /* PROPS */, _hoisted_61)], 64 /* STABLE_FRAGMENT */)) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"button\", {\n key: 1,\n type: \"button\",\n class: \"danger-inline-action\",\n disabled: $setup.groupBusy,\n onClick: $setup.leaveCurrentGroup\n }, \"退出群组\", 8 /* PROPS */, _hoisted_62))])])) : $setup.groupPanel === 'members' ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"section\", _hoisted_63, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"header\", null, [_cache[58] || (_cache[58] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"h3\", null, \"群员\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: $setup.closeGroupPanel\n }, \"×\")]), $setup.groupBusy ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_64, \"正在加载\")) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_65, [((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_1__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.renderList)($setup.groupMembers, member => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", {\n key: member.uid,\n class: \"group-member-row\"\n }, [member.portrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"img\", {\n key: 0,\n class: \"conversation-avatar image-avatar\",\n src: member.portrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_66)) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_67, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.memberAvatar(member)), 1 /* TEXT */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)(member.displayName), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"small\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)(member.role === 'owner' ? '群主' : member.rawDisplayName), 1 /* TEXT */)])]);\n }), 128 /* KEYED_FRAGMENT */)), !$setup.groupMembers.length ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_68, \"暂无群员\")) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true)]))])) : $setup.groupPanel === 'alias' ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"form\", {\n key: 2,\n class: \"group-dialog\",\n onSubmit: (0,vue__WEBPACK_IMPORTED_MODULE_1__.withModifiers)($setup.saveGroupAlias, [\"prevent\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"header\", null, [_cache[59] || (_cache[59] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"h3\", null, \"我在本群的昵称\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: $setup.closeGroupPanel\n }, \"×\")]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"label\", null, [_cache[60] || (_cache[60] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, \"昵称\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[15] || (_cache[15] = $event => $setup.groupAliasDraft = $event),\n maxlength: \"64\",\n autocomplete: \"off\",\n placeholder: \"留空使用个人昵称\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_1__.vModelText, $setup.groupAliasDraft, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"footer\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"ghost-inline-action\",\n onClick: $setup.closeGroupPanel\n }, \"取消\"), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n class: \"primary-inline-action\",\n disabled: $setup.groupBusy\n }, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.groupBusy ? '保存中...' : '保存'), 9 /* TEXT, PROPS */, _hoisted_69)])], 32 /* NEED_HYDRATION */)) : $setup.groupPanel === 'transfer' ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"section\", _hoisted_70, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"header\", null, [_cache[61] || (_cache[61] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"h3\", null, \"转让群组\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: $setup.closeGroupPanel\n }, \"×\")]), $setup.groupBusy ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_71, \"正在加载\")) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_72, [((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_1__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.renderList)($setup.transferableMembers, member => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"button\", {\n key: member.uid,\n type: \"button\",\n class: (0,vue__WEBPACK_IMPORTED_MODULE_1__.normalizeClass)([\"group-member-row selectable\", {\n selected: $setup.transferTargetId === member.uid\n }]),\n onClick: $event => $setup.transferTargetId = member.uid\n }, [member.portrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"img\", {\n key: 0,\n class: \"conversation-avatar image-avatar\",\n src: member.portrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_74)) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_75, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.memberAvatar(member)), 1 /* TEXT */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)(member.displayName), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"small\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)(member.rawDisplayName), 1 /* TEXT */)])], 10 /* CLASS, PROPS */, _hoisted_73);\n }), 128 /* KEYED_FRAGMENT */)), !$setup.transferableMembers.length ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_76, \"暂无可转让成员\")) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true)])), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"footer\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"ghost-inline-action\",\n onClick: $setup.closeGroupPanel\n }, \"取消\"), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"primary-inline-action\",\n disabled: $setup.groupBusy || !$setup.transferTargetId,\n onClick: $setup.transferCurrentGroup\n }, \"确认转让\", 8 /* PROPS */, _hoisted_77)])])) : $setup.groupPanel === 'manage' ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"section\", _hoisted_78, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"header\", null, [_cache[62] || (_cache[62] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"h3\", null, \"群组管理\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: $setup.closeGroupPanel\n }, \"×\")]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"nav\", _hoisted_79, [$setup.isGroupOwner ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"button\", {\n key: 0,\n type: \"button\",\n class: (0,vue__WEBPACK_IMPORTED_MODULE_1__.normalizeClass)({\n active: $setup.manageSection === 'admins'\n }),\n onClick: _cache[16] || (_cache[16] = $event => $setup.manageSection = 'admins')\n }, \"管理员设置\", 2 /* CLASS */)) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: (0,vue__WEBPACK_IMPORTED_MODULE_1__.normalizeClass)({\n active: $setup.manageSection === 'profile'\n }),\n onClick: _cache[17] || (_cache[17] = $event => $setup.manageSection = 'profile')\n }, \"修改群资料\", 2 /* CLASS */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: (0,vue__WEBPACK_IMPORTED_MODULE_1__.normalizeClass)({\n active: $setup.manageSection === 'kick'\n }),\n onClick: _cache[18] || (_cache[18] = $event => $setup.manageSection = 'kick')\n }, \"踢人\", 2 /* CLASS */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: (0,vue__WEBPACK_IMPORTED_MODULE_1__.normalizeClass)({\n active: $setup.manageSection === 'mute'\n }),\n onClick: _cache[19] || (_cache[19] = $event => $setup.manageSection = 'mute')\n }, \"禁言设置\", 2 /* CLASS */)]), $setup.groupBusy ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_80, \"正在加载\")) : $setup.manageSection === 'admins' ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"section\", _hoisted_81, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", _hoisted_82, [((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_1__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.renderList)($setup.adminCandidateMembers, member => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"label\", {\n key: member.uid,\n class: \"group-member-row selectable with-check\"\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"input\", {\n type: \"checkbox\",\n checked: $setup.selectedAdminIds.includes(member.uid),\n onChange: $event => $setup.toggleAdmin(member.uid)\n }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_83), member.portrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"img\", {\n key: 0,\n class: \"conversation-avatar image-avatar\",\n src: member.portrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_84)) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_85, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.memberAvatar(member)), 1 /* TEXT */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)(member.displayName), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"small\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)(member.rawDisplayName), 1 /* TEXT */)])]);\n }), 128 /* KEYED_FRAGMENT */)), !$setup.adminCandidateMembers.length ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_86, \"暂无可设置成员\")) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"footer\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"primary-inline-action\",\n onClick: $setup.saveGroupAdmins\n }, \"保存管理员\")])])) : $setup.manageSection === 'profile' ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"section\", _hoisted_87, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"label\", null, [_cache[63] || (_cache[63] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, \"群名称\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[20] || (_cache[20] = $event => $setup.profileDraft.name = $event),\n maxlength: \"128\",\n autocomplete: \"off\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_1__.vModelText, $setup.profileDraft.name, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"label\", null, [_cache[64] || (_cache[64] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, \"群说明\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"textarea\", {\n \"onUpdate:modelValue\": _cache[21] || (_cache[21] = $event => $setup.profileDraft.announcement = $event),\n maxlength: \"1000\",\n rows: \"3\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_1__.vModelText, $setup.profileDraft.announcement, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"label\", null, [_cache[65] || (_cache[65] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, \"欢迎词\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"textarea\", {\n \"onUpdate:modelValue\": _cache[22] || (_cache[22] = $event => $setup.profileDraft.welcomeText = $event),\n maxlength: \"1000\",\n rows: \"3\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_1__.vModelText, $setup.profileDraft.welcomeText, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"footer\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"primary-inline-action\",\n onClick: $setup.saveGroupProfile\n }, \"保存资料\")])])) : $setup.manageSection === 'kick' ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"section\", _hoisted_88, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"div\", _hoisted_89, [((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_1__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.renderList)($setup.kickCandidateMembers, member => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"label\", {\n key: member.uid,\n class: \"group-member-row selectable with-check\"\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"input\", {\n type: \"checkbox\",\n checked: $setup.selectedKickIds.includes(member.uid),\n onChange: $event => $setup.toggleKick(member.uid)\n }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_90), member.portrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"img\", {\n key: 0,\n class: \"conversation-avatar image-avatar\",\n src: member.portrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_91)) : ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_92, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.memberAvatar(member)), 1 /* TEXT */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)(member.displayName), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"small\", null, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)(member.role === 'admin' ? '管理员' : member.rawDisplayName), 1 /* TEXT */)])]);\n }), 128 /* KEYED_FRAGMENT */)), !$setup.kickCandidateMembers.length ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_93, \"暂无可移除成员\")) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true)]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"footer\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"danger-inline-action\",\n disabled: !$setup.selectedKickIds.length,\n onClick: $setup.kickSelectedMembers\n }, \"移出群组\", 8 /* PROPS */, _hoisted_94)])])) : $setup.manageSection === 'mute' ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"section\", _hoisted_95, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"label\", null, [_cache[67] || (_cache[67] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, \"禁言模式\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"select\", {\n \"onUpdate:modelValue\": _cache[23] || (_cache[23] = $event => $setup.muteDraft.mode = $event),\n onChange: $setup.clearMuteSaveFeedback\n }, _cache[66] || (_cache[66] = [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"option\", {\n value: \"off\"\n }, \"关闭禁言\", -1 /* HOISTED */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"option\", {\n value: \"all_include_admins\"\n }, \"全体禁言包含管理员\", -1 /* HOISTED */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"option\", {\n value: \"all_except_admins\"\n }, \"除管理员外全体禁言\", -1 /* HOISTED */)]), 544 /* NEED_HYDRATION, NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_1__.vModelSelect, $setup.muteDraft.mode]])]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"label\", null, [_cache[69] || (_cache[69] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, \"时段类型\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"select\", {\n \"onUpdate:modelValue\": _cache[24] || (_cache[24] = $event => $setup.muteDraft.scheduleType = $event),\n onChange: $setup.clearMuteSaveFeedback\n }, _cache[68] || (_cache[68] = [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"option\", {\n value: \"manual\"\n }, \"立即禁言直到管理解除\", -1 /* HOISTED */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"option\", {\n value: \"once\"\n }, \"单次\", -1 /* HOISTED */), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"option\", {\n value: \"daily\"\n }, \"每天固定时段\", -1 /* HOISTED */)]), 544 /* NEED_HYDRATION, NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_1__.vModelSelect, $setup.muteDraft.scheduleType]])]), $setup.muteDraft.mode !== 'off' && $setup.muteDraft.scheduleType === 'once' ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_96, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"label\", null, [_cache[70] || (_cache[70] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, \"开始\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[25] || (_cache[25] = $event => $setup.muteDraft.startsAtLocal = $event),\n type: \"datetime-local\",\n onInput: $setup.clearMuteSaveFeedback\n }, null, 544 /* NEED_HYDRATION, NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_1__.vModelText, $setup.muteDraft.startsAtLocal]])]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"label\", null, [_cache[71] || (_cache[71] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, \"结束\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[26] || (_cache[26] = $event => $setup.muteDraft.endsAtLocal = $event),\n type: \"datetime-local\",\n onInput: $setup.clearMuteSaveFeedback\n }, null, 544 /* NEED_HYDRATION, NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_1__.vModelText, $setup.muteDraft.endsAtLocal]])])])) : $setup.muteDraft.mode !== 'off' && $setup.muteDraft.scheduleType === 'daily' ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", _hoisted_97, [(0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"label\", null, [_cache[72] || (_cache[72] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, \"开始\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[27] || (_cache[27] = $event => $setup.muteDraft.dailyStart = $event),\n type: \"time\",\n onInput: $setup.clearMuteSaveFeedback\n }, null, 544 /* NEED_HYDRATION, NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_1__.vModelText, $setup.muteDraft.dailyStart]])]), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"label\", null, [_cache[73] || (_cache[73] = (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"span\", null, \"结束\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_1__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[28] || (_cache[28] = $event => $setup.muteDraft.dailyEnd = $event),\n type: \"time\",\n onInput: $setup.clearMuteSaveFeedback\n }, null, 544 /* NEED_HYDRATION, NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_1__.vModelText, $setup.muteDraft.dailyEnd]])])])) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"footer\", _hoisted_98, [$setup.muteSaveNotice ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_99, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.muteSaveNotice), 1 /* TEXT */)) : $setup.muteSaveError ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"span\", _hoisted_100, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.muteSaveError), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"primary-inline-action\",\n disabled: $setup.groupBusy,\n onClick: $setup.saveMuteSetting\n }, \"保存禁言\", 8 /* PROPS */, _hoisted_101)])])) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true)])) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true)])) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true), $setup.conversations.actionNotice || $setup.conversations.actionError ? ((0,vue__WEBPACK_IMPORTED_MODULE_1__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_1__.createElementBlock)(\"div\", {\n key: 8,\n class: (0,vue__WEBPACK_IMPORTED_MODULE_1__.normalizeClass)([\"chat-toast\", {\n error: $setup.conversations.actionError\n }])\n }, (0,vue__WEBPACK_IMPORTED_MODULE_1__.toDisplayString)($setup.conversations.actionError || $setup.conversations.actionNotice), 3 /* TEXT, CLASS */)) : (0,vue__WEBPACK_IMPORTED_MODULE_1__.createCommentVNode)(\"v-if\", true)]);\n}\n\n//# sourceURL=webpack://jc-web-im/./src/components/chat/ChatPanel.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B3%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/MessageComposer.vue?vue&type=template&id=08eca580": +/*!**********************************************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/MessageComposer.vue?vue&type=template&id=08eca580 ***! + \**********************************************************************************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* binding */ render; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n\nconst _hoisted_1 = {\n key: 0,\n class: \"quote-preview\"\n};\nconst _hoisted_2 = {\n class: \"composer-tools\"\n};\nconst _hoisted_3 = [\"disabled\"];\nconst _hoisted_4 = [\"disabled\"];\nconst _hoisted_5 = [\"disabled\"];\nconst _hoisted_6 = [\"onClick\"];\nconst _hoisted_7 = [\"disabled\", \"onKeydown\"];\nconst _hoisted_8 = {\n class: \"composer-footer\"\n};\nconst _hoisted_9 = [\"disabled\"];\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"form\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)([\"message-composer\", {\n 'drop-active': $setup.dropActive\n }]),\n onSubmit: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.submit, [\"prevent\"]),\n onDragenter: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.handleDragEnter, [\"prevent\"]),\n onDragover: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.handleDragOver, [\"prevent\"]),\n onDragleave: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.handleDragLeave, [\"prevent\"]),\n onDrop: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.handleDrop, [\"prevent\"]),\n onPaste: $setup.handlePaste\n }, [$props.quoteMessage ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, [_cache[5] || (_cache[5] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, \"引用\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createTextVNode)(\" \" + (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($props.quoteMessage.quoteText || $props.quoteMessage.text), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"取消引用\",\n onClick: _cache[0] || (_cache[0] = $event => _ctx.$emit('cancel-quote'))\n }, \"×\")])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_2, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"表情\",\n disabled: $props.disabled,\n onClick: $setup.toggleEmojiPicker\n }, \"☺\", 8 /* PROPS */, _hoisted_3), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"图片\",\n disabled: $props.disabled,\n onClick: $setup.chooseImage\n }, \"▧\", 8 /* PROPS */, _hoisted_4), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"文件\",\n disabled: $props.disabled,\n onClick: $setup.chooseFile\n }, \"▤\", 8 /* PROPS */, _hoisted_5), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n ref: \"imageInput\",\n type: \"file\",\n accept: \"image/*\",\n multiple: \"\",\n onChange: _cache[1] || (_cache[1] = $event => $setup.selectAttachment($event, 'image'))\n }, null, 544 /* NEED_HYDRATION, NEED_PATCH */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n ref: \"fileInput\",\n type: \"file\",\n multiple: \"\",\n onChange: _cache[2] || (_cache[2] = $event => $setup.selectAttachment($event, 'file'))\n }, null, 544 /* NEED_HYDRATION, NEED_PATCH */), $setup.emojiOpen ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", {\n key: 0,\n class: \"emoji-picker\",\n onClick: _cache[3] || (_cache[3] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)(() => {}, [\"stop\"]))\n }, [((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.renderList)($setup.emojiList, emoji => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n key: emoji,\n type: \"button\",\n onClick: $event => $setup.insertEmoji(emoji)\n }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)(emoji), 9 /* TEXT, PROPS */, _hoisted_6);\n }), 64 /* STABLE_FRAGMENT */))])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"textarea\", {\n ref: \"textareaEl\",\n \"onUpdate:modelValue\": _cache[4] || (_cache[4] = $event => $setup.text = $event),\n disabled: $props.disabled,\n placeholder: \"输入消息\",\n rows: \"3\",\n onKeydown: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withKeys)((0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.submit, [\"exact\", \"prevent\"]), [\"enter\"])\n }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_7), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.text]]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_8, [_cache[6] || (_cache[6] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"Enter 发送,Shift + Enter 换行\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: \"send-button\",\n disabled: $props.disabled || !$setup.text.trim()\n }, \"发送\", 8 /* PROPS */, _hoisted_9)])], 34 /* CLASS, NEED_HYDRATION */);\n}\n\n//# sourceURL=webpack://jc-web-im/./src/components/chat/MessageComposer.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B3%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/MessageList.vue?vue&type=template&id=a39e6c04": +/*!******************************************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/MessageList.vue?vue&type=template&id=a39e6c04 ***! + \******************************************************************************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* binding */ render; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n\nconst _hoisted_1 = {\n ref: \"listEl\",\n class: \"message-list\"\n};\nconst _hoisted_2 = [\"data-message-key\", \"onClick\"];\nconst _hoisted_3 = {\n key: 0,\n class: \"message-select\"\n};\nconst _hoisted_4 = [\"checked\", \"onChange\"];\nconst _hoisted_5 = [\"title\", \"onClick\"];\nconst _hoisted_6 = [\"src\"];\nconst _hoisted_7 = {\n key: 1\n};\nconst _hoisted_8 = {\n class: \"message-stack\"\n};\nconst _hoisted_9 = [\"onClick\"];\nconst _hoisted_10 = {\n key: 0,\n class: \"message-sender-role\"\n};\nconst _hoisted_11 = [\"onContextmenu\"];\nconst _hoisted_12 = [\"onClick\"];\nconst _hoisted_13 = [\"src\", \"alt\"];\nconst _hoisted_14 = [\"href\"];\nconst _hoisted_15 = {\n class: \"message-file-main\"\n};\nconst _hoisted_16 = {\n key: 2\n};\nconst _hoisted_17 = {\n key: 0,\n class: \"recall-mark\"\n};\nconst _hoisted_18 = {\n key: 1,\n class: \"edit-mark\"\n};\nconst _hoisted_19 = {\n key: 2,\n class: \"favorite-mark\",\n title: \"已收藏\",\n \"aria-label\": \"已收藏\"\n};\nconst _hoisted_20 = [\"title\", \"aria-label\"];\nconst _hoisted_21 = {\n key: 0,\n class: \"delivery-check delivery-check-second\"\n};\nconst _hoisted_22 = {\n key: 4,\n class: \"delivery-error\"\n};\nconst _hoisted_23 = [\"onClick\"];\nconst _hoisted_24 = {\n class: \"image-viewer-toolbar\"\n};\nconst _hoisted_25 = {\n class: \"image-viewer-title\"\n};\nconst _hoisted_26 = [\"src\", \"alt\"];\nconst _hoisted_27 = {\n class: \"sender-profile-main\"\n};\nconst _hoisted_28 = [\"src\"];\nconst _hoisted_29 = {\n key: 1,\n class: \"profile-avatar\"\n};\nconst _hoisted_30 = {\n key: 2\n};\nconst _hoisted_31 = {\n class: \"sender-profile-fields\"\n};\nconst _hoisted_32 = {\n class: \"sender-profile-alias\"\n};\nconst _hoisted_33 = [\"disabled\"];\nconst _hoisted_34 = {\n class: \"sender-profile-actions\"\n};\nconst _hoisted_35 = {\n key: 0,\n class: \"inline-save-status\"\n};\nconst _hoisted_36 = {\n key: 1,\n class: \"inline-save-status error\"\n};\nconst _hoisted_37 = [\"disabled\"];\nconst _hoisted_38 = [\"disabled\"];\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_1, [((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.renderList)($props.messages, message => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"article\", {\n key: message.id,\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)([\"message-row\", [message.direction, {\n selected: $setup.isSelected(message),\n attention: $setup.isAttention(message),\n recalled: message.recalled || message.status === 'recalled'\n }]]),\n \"data-message-key\": $setup.messageIdentity(message),\n onClick: $event => $setup.handleMessageClick(message)\n }, [$props.selectionMode ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"label\", _hoisted_3, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n type: \"checkbox\",\n checked: $setup.isSelected(message),\n onClick: _cache[0] || (_cache[0] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)(() => {}, [\"stop\"])),\n onChange: $event => $setup.emit('toggle-select', message)\n }, null, 40 /* PROPS, NEED_HYDRATION */, _hoisted_4)])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), $setup.showGroupSender(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"button\", {\n key: 1,\n type: \"button\",\n class: \"message-sender-avatar\",\n title: $setup.groupSenderName(message),\n onClick: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($event => $setup.openSenderProfile(message), [\"stop\"])\n }, [message.senderPortrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"img\", {\n key: 0,\n src: message.senderPortrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_6)) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_7, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.groupSenderAvatarText(message)), 1 /* TEXT */))], 8 /* PROPS */, _hoisted_5)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_8, [$setup.showGroupSender(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"button\", {\n key: 0,\n type: \"button\",\n class: \"message-sender-name\",\n onClick: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($event => $setup.openSenderProfile(message), [\"stop\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createTextVNode)((0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.groupSenderName(message)) + \" \", 1 /* TEXT */), $setup.senderRoleLabel(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_10, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.senderRoleLabel(message)), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)], 8 /* PROPS */, _hoisted_9)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)([\"message-bubble\", $setup.roleBubbleClass(message)]),\n onContextmenu: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($event => $setup.openMenu($event, message), [\"prevent\"])\n }, [$setup.isImageMessage(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"button\", {\n key: 0,\n type: \"button\",\n class: \"message-image\",\n onClick: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($event => $setup.handleImageClick(message), [\"stop\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"img\", {\n src: $setup.imageDisplayUrl(message.attachment.url),\n alt: message.attachment.name || '图片'\n }, null, 8 /* PROPS */, _hoisted_13)], 8 /* PROPS */, _hoisted_12)) : $setup.isFileMessage(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"a\", {\n key: 1,\n class: \"message-file\",\n href: message.attachment.url,\n target: \"_blank\",\n rel: \"noreferrer\",\n download: \"\"\n }, [_cache[7] || (_cache[7] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"message-file-icon\"\n }, \"▤\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", _hoisted_15, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)(message.attachment.name || '文件'), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"small\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.formatFileSize(message.attachment.size)), 1 /* TEXT */)])], 8 /* PROPS */, _hoisted_14)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), message.text ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"p\", _hoisted_16, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)(message.text), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"time\", null, [$setup.isRecallNotice(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_17, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)(message.recallStatus || '已撤回'), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.formatTime(message.displayTimestamp || message.timestamp)), 1 /* TEXT */), message.editedAt ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_18, \"【edit \" + (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.formatTime(message.editedAt)) + \"】\", 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), $setup.isFavorite(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_19, \"★\")) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), $setup.deliveryState(message) ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", {\n key: 3,\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)([\"delivery-mark\", `delivery-mark-${$setup.deliveryState(message)}`]),\n title: $setup.deliveryTitle(message),\n \"aria-label\": $setup.deliveryTitle(message)\n }, [_cache[8] || (_cache[8] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"delivery-check\"\n }, \"✓\", -1 /* HOISTED */)), $setup.deliveryState(message) !== 'sent' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_21, \"✓\")) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)], 10 /* CLASS, PROPS */, _hoisted_20)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), message.status === 'failed' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_22, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.failureText(message)), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)])], 42 /* CLASS, PROPS, NEED_HYDRATION */, _hoisted_11)])], 10 /* CLASS, PROPS */, _hoisted_2);\n }), 128 /* KEYED_FRAGMENT */)), $setup.contextMenu.visible ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", {\n key: 0,\n class: \"message-context-menu\",\n style: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeStyle)({\n left: `${$setup.contextMenu.x}px`,\n top: `${$setup.contextMenu.y}px`\n }),\n onClick: _cache[1] || (_cache[1] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)(() => {}, [\"stop\"]))\n }, [((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.renderList)($setup.menuItems, item => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"button\", {\n key: item.action,\n type: \"button\",\n onClick: $event => $setup.chooseMenuAction(item.action)\n }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)(item.label), 9 /* TEXT, PROPS */, _hoisted_23);\n }), 128 /* KEYED_FRAGMENT */))], 4 /* STYLE */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Teleport, {\n to: \"body\"\n }, [$setup.imageViewer.visible ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", {\n key: 0,\n class: \"image-viewer-backdrop\",\n onClick: $setup.closeImageViewer\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n class: \"image-viewer-dialog\",\n onClick: _cache[4] || (_cache[4] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)(() => {}, [\"stop\"]))\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_24, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", _hoisted_25, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.imageViewer.name || '图片'), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"缩小\",\n \"aria-label\": \"缩小\",\n onClick: _cache[2] || (_cache[2] = $event => $setup.zoomImage(-0.25))\n }, \"−\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"放大\",\n \"aria-label\": \"放大\",\n onClick: _cache[3] || (_cache[3] = $event => $setup.zoomImage(0.25))\n }, \"+\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"还原\",\n \"aria-label\": \"还原\",\n onClick: $setup.resetImageViewer\n }, \"1:1\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"旋转\",\n \"aria-label\": \"旋转\",\n onClick: $setup.rotateImage\n }, \"⟳\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"保存\",\n \"aria-label\": \"保存\",\n onClick: $setup.saveViewerImage\n }, \"⇩\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"关闭\",\n \"aria-label\": \"关闭\",\n onClick: $setup.closeImageViewer\n }, \"×\")]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n ref: \"viewerStage\",\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)([\"image-viewer-stage\", {\n dragging: $setup.imageViewer.dragging,\n zoomed: $setup.imageViewer.scale > 1\n }]),\n onWheel: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.handleImageWheel, [\"prevent\"]),\n onPointerdown: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.startImageDrag, [\"prevent\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"img\", {\n class: \"image-viewer-img\",\n src: $setup.imageViewer.url,\n alt: $setup.imageViewer.name || '图片',\n style: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeStyle)($setup.imageViewerStyle),\n draggable: \"false\"\n }, null, 12 /* STYLE, PROPS */, _hoisted_26)], 34 /* CLASS, NEED_HYDRATION */)])])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)])), ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Teleport, {\n to: \"body\"\n }, [$setup.senderProfile.visible ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", {\n key: 0,\n class: \"sender-profile-backdrop\",\n onClick: $setup.closeSenderProfile\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"section\", {\n class: \"sender-profile-dialog\",\n onClick: _cache[6] || (_cache[6] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)(() => {}, [\"stop\"]))\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"header\", null, [_cache[9] || (_cache[9] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, \"用户资料\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: $setup.closeSenderProfile\n }, \"×\")]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_27, [$setup.senderProfile.portrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"img\", {\n key: 0,\n class: \"profile-avatar image-avatar\",\n src: $setup.senderProfile.portrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_28)) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_29, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.senderProfile.avatarText), 1 /* TEXT */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.senderProfile.displayName || $setup.senderProfile.uid), 1 /* TEXT */), $setup.senderProfile.roleLabel ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_30, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.senderProfile.roleLabel), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"dl\", _hoisted_31, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", null, [_cache[10] || (_cache[10] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"dt\", null, \"用户ID\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.senderProfile.uid || '-'), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", null, [_cache[11] || (_cache[11] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"dt\", null, \"昵称\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.senderProfile.rawName || $setup.senderProfile.displayName || '-'), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", null, [_cache[12] || (_cache[12] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"dt\", null, \"手机号\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.senderProfile.mobile || '未知'), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", null, [_cache[13] || (_cache[13] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"dt\", null, \"地区\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.senderProfile.address || '未知'), 1 /* TEXT */)])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", _hoisted_32, [_cache[14] || (_cache[14] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"备注名称\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[5] || (_cache[5] = $event => $setup.senderProfile.aliasDraft = $event),\n maxlength: \"32\",\n autocomplete: \"off\",\n placeholder: \"可给群员设置备注\",\n disabled: $setup.senderProfile.saving || $setup.senderProfile.adding\n }, null, 8 /* PROPS */, _hoisted_33), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.senderProfile.aliasDraft, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"footer\", _hoisted_34, [$setup.senderProfile.notice ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_35, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.senderProfile.notice), 1 /* TEXT */)) : $setup.senderProfile.error ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_36, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.senderProfile.error), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), !$setup.senderProfile.isContact ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"button\", {\n key: 2,\n type: \"button\",\n class: \"ghost-inline-action\",\n disabled: $setup.senderProfile.adding || $setup.senderProfile.saving || !$setup.senderProfile.uid,\n onClick: $setup.addSenderAsContact\n }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.senderProfile.adding ? '添加中...' : '添加为好友'), 9 /* TEXT, PROPS */, _hoisted_37)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"primary-inline-action\",\n disabled: $setup.senderProfile.saving || $setup.senderProfile.adding || !$setup.senderProfile.uid,\n onClick: $setup.saveSenderAlias\n }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.senderProfile.saving ? '保存中...' : '保存备注'), 9 /* TEXT, PROPS */, _hoisted_38)])])])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)]))], 512 /* NEED_PATCH */);\n}\n\n//# sourceURL=webpack://jc-web-im/./src/components/chat/MessageList.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B3%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/contacts/ContactPanel.vue?vue&type=template&id=63d88b80": +/*!***********************************************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/contacts/ContactPanel.vue?vue&type=template&id=63d88b80 ***! + \***********************************************************************************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* binding */ render; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n\nconst _hoisted_1 = {\n class: \"contact-panel\"\n};\nconst _hoisted_2 = {\n class: \"chat-header\"\n};\nconst _hoisted_3 = {\n class: \"contact-profile\"\n};\nconst _hoisted_4 = [\"src\"];\nconst _hoisted_5 = {\n key: 1,\n class: \"profile-avatar\"\n};\nconst _hoisted_6 = {\n class: \"profile-main\"\n};\nconst _hoisted_7 = {\n class: \"remark-row\"\n};\nconst _hoisted_8 = {\n class: \"profile-fields\"\n};\nconst _hoisted_9 = {\n key: 0,\n class: \"form-error\"\n};\nconst _hoisted_10 = {\n class: \"modal-actions\"\n};\nconst _hoisted_11 = [\"disabled\"];\nconst _hoisted_12 = [\"disabled\"];\nconst _hoisted_13 = {\n key: 1,\n class: \"empty-chat\"\n};\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"section\", _hoisted_1, [$setup.contact ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, {\n key: 0\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"header\", _hoisted_2, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"h2\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.remarkName), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"p\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.username), 1 /* TEXT */)])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_3, [$setup.contact.portrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"img\", {\n key: 0,\n class: \"profile-avatar image-avatar\",\n src: $setup.contact.portrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_4)) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_5, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.contact.avatarText), 1 /* TEXT */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_6, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_7, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"h3\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.remarkName), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button remark-edit-button\",\n title: \"编辑备注名\",\n onClick: $setup.openAliasEditor\n }, \"✏️\")])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"dl\", _hoisted_8, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", null, [_cache[2] || (_cache[2] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"dt\", null, \"用户名\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.username), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", null, [_cache[3] || (_cache[3] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"dt\", null, \"手机号\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.mobile), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", null, [_cache[4] || (_cache[4] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"dt\", null, \"地区\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"dd\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.address), 1 /* TEXT */)])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: \"primary-action profile-action\",\n onClick: _cache[0] || (_cache[0] = $event => $setup.contacts.openChat($setup.contact))\n }, \"发消息\")]), $setup.editingAlias ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", {\n key: 0,\n class: \"modal-backdrop contact-modal-backdrop\",\n onClick: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.closeAliasEditor, [\"self\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"form\", {\n class: \"modal-panel\",\n onSubmit: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.submitAlias, [\"prevent\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"header\", null, [_cache[5] || (_cache[5] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, \"编辑备注名\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: $setup.closeAliasEditor\n }, \"×\")]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[6] || (_cache[6] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"备注名\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[1] || (_cache[1] = $event => $setup.aliasDraft = $event),\n maxlength: \"32\",\n autocomplete: \"off\",\n autofocus: \"\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.aliasDraft, void 0, {\n trim: true\n }]])]), $setup.aliasError ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"p\", _hoisted_9, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.aliasError), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_10, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"ghost-inline-action\",\n disabled: $setup.aliasSaving,\n onClick: $setup.closeAliasEditor\n }, \"取消\", 8 /* PROPS */, _hoisted_11), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"submit\",\n class: \"primary-inline-action\",\n disabled: $setup.aliasSaving\n }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.aliasSaving ? '保存中' : '保存'), 9 /* TEXT, PROPS */, _hoisted_12)])], 32 /* NEED_HYDRATION */)])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)], 64 /* STABLE_FRAGMENT */)) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_13, _cache[7] || (_cache[7] = [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, \"选择一个联系人\", -1 /* HOISTED */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"好友列表同步完成后,可以在左侧查看资料\", -1 /* HOISTED */)])))]);\n}\n\n//# sourceURL=webpack://jc-web-im/./src/components/contacts/ContactPanel.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B3%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/favorites/FavoriteList.vue?vue&type=template&id=0086db7c": +/*!************************************************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/favorites/FavoriteList.vue?vue&type=template&id=0086db7c ***! + \************************************************************************************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* binding */ render; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n\nconst _hoisted_1 = {\n class: \"conversation-list favorite-list\"\n};\nconst _hoisted_2 = {\n class: \"list-header\"\n};\nconst _hoisted_3 = {\n class: \"sidebar-scroll\"\n};\nconst _hoisted_4 = [\"onClick\"];\nconst _hoisted_5 = {\n class: \"favorite-item-main\"\n};\nconst _hoisted_6 = {\n key: 0,\n class: \"empty-list\"\n};\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"aside\", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"header\", _hoisted_2, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", null, [_cache[0] || (_cache[0] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"h2\", null, \"收藏\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"p\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.conversations.favoriteMessages.length) + \" 条消息\", 1 /* TEXT */)])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_3, [((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.renderList)($setup.conversations.favoriteMessages, item => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"button\", {\n key: item.key,\n type: \"button\",\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)([\"favorite-item\", {\n active: $setup.activeKey === item.key\n }]),\n onClick: $event => $setup.conversations.setActiveFavorite(item.key)\n }, [_cache[1] || (_cache[1] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"favorite-item-icon\"\n }, \"★\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", _hoisted_5, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.favoriteTitle(item)), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"small\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.favoritePreview(item)), 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"time\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.formatFavoriteTime(item.timestamp)), 1 /* TEXT */)], 10 /* CLASS, PROPS */, _hoisted_4);\n }), 128 /* KEYED_FRAGMENT */)), !$setup.conversations.favoriteMessages.length ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_6, \" 暂无收藏消息 \")) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)])]);\n}\n\n//# sourceURL=webpack://jc-web-im/./src/components/favorites/FavoriteList.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B3%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/favorites/FavoritePanel.vue?vue&type=template&id=29e1a3e0": +/*!*************************************************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/favorites/FavoritePanel.vue?vue&type=template&id=29e1a3e0 ***! + \*************************************************************************************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* binding */ render; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n\nconst _hoisted_1 = {\n class: \"favorite-panel\"\n};\nconst _hoisted_2 = {\n class: \"chat-header\"\n};\nconst _hoisted_3 = {\n class: \"favorite-detail\"\n};\nconst _hoisted_4 = {\n class: \"favorite-detail-body\"\n};\nconst _hoisted_5 = {\n key: 0,\n class: \"recall-mark\"\n};\nconst _hoisted_6 = [\"href\"];\nconst _hoisted_7 = [\"src\", \"alt\"];\nconst _hoisted_8 = [\"href\"];\nconst _hoisted_9 = {\n class: \"message-file-main\"\n};\nconst _hoisted_10 = {\n key: 3\n};\nconst _hoisted_11 = {\n class: \"favorite-detail-actions\"\n};\nconst _hoisted_12 = {\n key: 0,\n class: \"inline-save-status\"\n};\nconst _hoisted_13 = {\n key: 1,\n class: \"inline-save-status error\"\n};\nconst _hoisted_14 = {\n key: 1,\n class: \"empty-chat\"\n};\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"section\", _hoisted_1, [$setup.favorite ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, {\n key: 0\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"header\", _hoisted_2, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", null, [_cache[0] || (_cache[0] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"h2\", null, \"收藏详情\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"p\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.favoriteTitle($setup.favorite)), 1 /* TEXT */)])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_3, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)([\"favorite-detail-message\", {\n recalled: $setup.favorite.recalled\n }])\n }, [_cache[2] || (_cache[2] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"favorite-detail-star\"\n }, \"★\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_4, [$setup.favorite.recalled ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_5, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.favorite.recallStatus || '已撤回'), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), $setup.isImageFavorite($setup.favorite) ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"a\", {\n key: 1,\n class: \"favorite-detail-image\",\n href: $setup.favorite.attachment.url,\n target: \"_blank\",\n rel: \"noreferrer\"\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"img\", {\n src: $setup.imageDisplayUrl($setup.favorite.attachment.url),\n alt: $setup.favorite.attachment.name || '图片'\n }, null, 8 /* PROPS */, _hoisted_7)], 8 /* PROPS */, _hoisted_6)) : $setup.isFileFavorite($setup.favorite) ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"a\", {\n key: 2,\n class: \"message-file favorite-detail-file\",\n href: $setup.favorite.attachment.url,\n target: \"_blank\",\n rel: \"noreferrer\",\n download: \"\"\n }, [_cache[1] || (_cache[1] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", {\n class: \"message-file-icon\"\n }, \"▤\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", _hoisted_9, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.favorite.attachment.name || '文件'), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"small\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.formatFileSize($setup.favorite.attachment.size)), 1 /* TEXT */)])], 8 /* PROPS */, _hoisted_8)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), $setup.favoriteText($setup.favorite) ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"p\", _hoisted_10, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.favoriteText($setup.favorite)), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"time\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.formatFavoriteTime($setup.favorite.timestamp)), 1 /* TEXT */)], 2 /* CLASS */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_11, [$setup.notice ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_12, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.notice), 1 /* TEXT */)) : $setup.error ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_13, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.error), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"ghost-inline-action\",\n onClick: $setup.copyFavorite\n }, \"复制\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"ghost-inline-action\",\n onClick: $setup.openConversation\n }, \"打开原会话\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"danger-inline-action\",\n onClick: $setup.removeFavorite\n }, \"取消收藏\")])])], 64 /* STABLE_FRAGMENT */)) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_14, _cache[3] || (_cache[3] = [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, \"暂无收藏消息\", -1 /* HOISTED */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"右键消息气泡选择“收藏”后,会显示在这里\", -1 /* HOISTED */)])))]);\n}\n\n//# sourceURL=webpack://jc-web-im/./src/components/favorites/FavoritePanel.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B3%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/ContactList.vue?vue&type=template&id=7aaf5f2b": +/*!*********************************************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/ContactList.vue?vue&type=template&id=7aaf5f2b ***! + \*********************************************************************************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* binding */ render; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n\nconst _hoisted_1 = {\n class: \"conversation-list\"\n};\nconst _hoisted_2 = {\n class: \"list-header\"\n};\nconst _hoisted_3 = {\n class: \"header-actions\"\n};\nconst _hoisted_4 = {\n class: \"search-box\"\n};\nconst _hoisted_5 = {\n class: \"sidebar-scroll\"\n};\nconst _hoisted_6 = [\"onClick\"];\nconst _hoisted_7 = [\"src\"];\nconst _hoisted_8 = {\n key: 1,\n class: \"conversation-avatar\"\n};\nconst _hoisted_9 = {\n class: \"conversation-copy\"\n};\nconst _hoisted_10 = {\n key: 0,\n class: \"empty-list\"\n};\nconst _hoisted_11 = {\n key: 0,\n class: \"form-error\"\n};\nconst _hoisted_12 = {\n key: 1,\n class: \"form-info\"\n};\nconst _hoisted_13 = {\n class: \"modal-actions\"\n};\nconst _hoisted_14 = [\"disabled\"];\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"aside\", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"header\", _hoisted_2, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", null, [_cache[3] || (_cache[3] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"h2\", null, \"联系人\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"p\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.contacts.contacts.length) + \" 位好友\", 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_3, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: \"icon-button\",\n title: \"添加联系人\",\n onClick: $setup.openAddContact\n }, \"+\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: \"icon-button\",\n title: \"刷新\",\n onClick: _cache[0] || (_cache[0] = $event => $setup.contacts.refreshContacts({\n force: true\n }))\n }, \"↻\")])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_4, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[1] || (_cache[1] = $event => $setup.keyword = $event),\n placeholder: \"搜索联系人\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.keyword, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_5, [((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.renderList)($setup.filteredContacts, item => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"button\", {\n key: item.id,\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)([\"conversation-item\", {\n active: item.id === $setup.contacts.activeId\n }]),\n onClick: $event => $setup.contacts.setActiveContact(item.id)\n }, [item.portrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"img\", {\n key: 0,\n class: \"conversation-avatar image-avatar\",\n src: item.portrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_7)) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_8, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)(item.avatarText), 1 /* TEXT */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", _hoisted_9, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)(item.title), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"small\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)(item.subtitle), 1 /* TEXT */)])], 10 /* CLASS, PROPS */, _hoisted_6);\n }), 128 /* KEYED_FRAGMENT */)), !$setup.filteredContacts.length ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_10, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.contacts.loading ? '正在加载' : '暂无联系人'), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.contacts.loading ? '请稍候' : '好友同步完成后会显示在这里'), 1 /* TEXT */)])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)]), $setup.addingContact ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", {\n key: 0,\n class: \"modal-backdrop\",\n onClick: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.closeAddContact, [\"self\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"form\", {\n class: \"modal-panel\",\n onSubmit: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.submitAddContact, [\"prevent\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"header\", null, [_cache[4] || (_cache[4] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, \"添加联系人\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: $setup.closeAddContact\n }, \"×\")]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[5] || (_cache[5] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"用户ID / 手机号 / 用户名\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[2] || (_cache[2] = $event => $setup.addKeyword = $event),\n autocomplete: \"off\",\n placeholder: \"请输入要添加的联系人\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.addKeyword, void 0, {\n trim: true\n }]])]), $setup.contacts.actionError ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"p\", _hoisted_11, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.contacts.actionError), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), $setup.contacts.actionInfo ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"p\", _hoisted_12, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.contacts.actionInfo), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_13, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"ghost-inline-action\",\n onClick: $setup.closeAddContact\n }, \"取消\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: \"primary-inline-action\",\n disabled: $setup.contacts.loading || !$setup.addKeyword\n }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.contacts.loading ? '添加中...' : '添加'), 9 /* TEXT, PROPS */, _hoisted_14)])], 32 /* NEED_HYDRATION */)])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)]);\n}\n\n//# sourceURL=webpack://jc-web-im/./src/components/sidebar/ContactList.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B3%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/ConversationList.vue?vue&type=template&id=7a656f78": +/*!**************************************************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/ConversationList.vue?vue&type=template&id=7a656f78 ***! + \**************************************************************************************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* binding */ render; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n\nconst _hoisted_1 = {\n class: \"conversation-list\"\n};\nconst _hoisted_2 = {\n class: \"list-header\"\n};\nconst _hoisted_3 = {\n class: \"header-actions action-menu-anchor\"\n};\nconst _hoisted_4 = {\n class: \"search-box\"\n};\nconst _hoisted_5 = {\n class: \"sidebar-scroll\"\n};\nconst _hoisted_6 = [\"onClick\"];\nconst _hoisted_7 = [\"src\"];\nconst _hoisted_8 = {\n key: 1,\n class: \"conversation-avatar\"\n};\nconst _hoisted_9 = {\n class: \"conversation-copy\"\n};\nconst _hoisted_10 = [\"aria-label\"];\nconst _hoisted_11 = {\n key: 0,\n class: \"empty-list\"\n};\nconst _hoisted_12 = {\n key: 1,\n class: \"empty-list\"\n};\nconst _hoisted_13 = {\n key: 0\n};\nconst _hoisted_14 = {\n key: 2\n};\nconst _hoisted_15 = {\n key: 5,\n class: \"form-error\"\n};\nconst _hoisted_16 = {\n key: 6,\n class: \"form-info\"\n};\nconst _hoisted_17 = {\n class: \"modal-actions\"\n};\nconst _hoisted_18 = [\"disabled\"];\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"aside\", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"header\", _hoisted_2, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", null, [_cache[18] || (_cache[18] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"h2\", null, \"消息\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"p\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.conversations.totalUnread) + \" 条未读\", 1 /* TEXT */)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_3, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: \"icon-button\",\n title: \"新建会话\",\n onClick: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.toggleActionMenu, [\"stop\"])\n }, \"+\"), $setup.actionMenuOpen ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", {\n key: 0,\n class: \"quick-action-menu\",\n onClick: _cache[5] || (_cache[5] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)(() => {}, [\"stop\"]))\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n onClick: _cache[0] || (_cache[0] = $event => $setup.openAction('addContact'))\n }, \"添加联系人\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n onClick: _cache[1] || (_cache[1] = $event => $setup.openAction('createGroup'))\n }, \"新建群组\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n onClick: _cache[2] || (_cache[2] = $event => $setup.openAction('joinGroup'))\n }, \"加入群组\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n onClick: _cache[3] || (_cache[3] = $event => $setup.openAction('broadcast'))\n }, \"群发助手\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n onClick: _cache[4] || (_cache[4] = $event => $setup.openAction('createChannel'))\n }, \"创建频道/公告群\")])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_4, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[6] || (_cache[6] = $event => $setup.searchKeyword = $event),\n placeholder: \"搜索会话、联系人\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.searchKeyword, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_5, [((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.renderList)($setup.filteredConversationItems, item => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"button\", {\n key: item.resultKey,\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)([\"conversation-item\", {\n active: $setup.isListItemActive(item)\n }]),\n onClick: $event => $setup.selectListItem(item)\n }, [item.portrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"img\", {\n key: 0,\n class: \"conversation-avatar image-avatar\",\n src: item.portrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_7)) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_8, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)(item.avatarText), 1 /* TEXT */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", _hoisted_9, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)(item.title), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"small\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)(item.subtitle), 1 /* TEXT */)]), Number(item.unread || 0) > 0 ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", {\n key: 2,\n class: \"unread-badge\",\n \"aria-label\": `${item.unread} 条未读消息`\n }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.formatUnread(item.unread)), 9 /* TEXT, PROPS */, _hoisted_10)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)], 10 /* CLASS, PROPS */, _hoisted_6);\n }), 128 /* KEYED_FRAGMENT */)), !$setup.filteredConversationItems.length && $setup.searchKeyword ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_11, _cache[19] || (_cache[19] = [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, \"没有匹配结果\", -1 /* HOISTED */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"换个关键词再试试\", -1 /* HOISTED */)]))) : !$setup.conversations.conversations.length && !$setup.conversations.usingDemoData ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_12, _cache[20] || (_cache[20] = [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, \"暂无会话\", -1 /* HOISTED */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"收到或发起消息后会出现在这里\", -1 /* HOISTED */)]))) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)]), $setup.activeAction ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", {\n key: 0,\n class: \"modal-backdrop\",\n onClick: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.closeActionModal, [\"self\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"form\", {\n class: \"modal-panel conversation-action-panel\",\n onSubmit: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.submitAction, [\"prevent\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"header\", null, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.actionTitle), 1 /* TEXT */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: $setup.closeActionModal\n }, \"×\")]), $setup.activeAction === 'addContact' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"label\", _hoisted_13, [_cache[21] || (_cache[21] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"用户ID / 手机号 / 用户名\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[7] || (_cache[7] = $event => $setup.addKeyword = $event),\n autocomplete: \"off\",\n placeholder: \"请输入要添加的联系人\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.addKeyword, void 0, {\n trim: true\n }]])])) : $setup.activeAction === 'createGroup' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, {\n key: 1\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[22] || (_cache[22] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"群组名称\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[8] || (_cache[8] = $event => $setup.groupName = $event),\n maxlength: \"64\",\n autocomplete: \"off\",\n placeholder: \"请输入群组名称\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.groupName, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[23] || (_cache[23] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"群说明\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"textarea\", {\n \"onUpdate:modelValue\": _cache[9] || (_cache[9] = $event => $setup.groupAnnouncement = $event),\n maxlength: \"1000\",\n rows: \"3\",\n placeholder: \"可选\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.groupAnnouncement, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[24] || (_cache[24] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"欢迎词\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"textarea\", {\n \"onUpdate:modelValue\": _cache[10] || (_cache[10] = $event => $setup.groupWelcomeText = $event),\n maxlength: \"1000\",\n rows: \"3\",\n placeholder: \"群组创建后自动发送\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.groupWelcomeText, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)($setup[\"ContactPicker\"], {\n contacts: $setup.filteredContacts,\n \"selected-ids\": $setup.selectedContactIds,\n keyword: $setup.contactKeyword,\n \"empty-text\": \"暂无可选联系人\",\n \"onUpdate:keyword\": _cache[11] || (_cache[11] = $event => $setup.contactKeyword = $event),\n onToggle: $setup.toggleContact\n }, null, 8 /* PROPS */, [\"contacts\", \"selected-ids\", \"keyword\"])], 64 /* STABLE_FRAGMENT */)) : $setup.activeAction === 'joinGroup' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"label\", _hoisted_14, [_cache[25] || (_cache[25] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"群组ID / 群名称\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[12] || (_cache[12] = $event => $setup.joinGroupKey = $event),\n autocomplete: \"off\",\n placeholder: \"请输入群组ID或群名称\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.joinGroupKey, void 0, {\n trim: true\n }]])])) : $setup.activeAction === 'broadcast' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, {\n key: 3\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[26] || (_cache[26] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"群发内容\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"textarea\", {\n \"onUpdate:modelValue\": _cache[13] || (_cache[13] = $event => $setup.broadcastText = $event),\n maxlength: \"2000\",\n rows: \"4\",\n placeholder: \"输入要群发的文字\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.broadcastText, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)($setup[\"ContactPicker\"], {\n contacts: $setup.filteredContacts,\n \"selected-ids\": $setup.selectedContactIds,\n keyword: $setup.contactKeyword,\n \"empty-text\": \"暂无可选联系人\",\n \"onUpdate:keyword\": _cache[14] || (_cache[14] = $event => $setup.contactKeyword = $event),\n onToggle: $setup.toggleContact\n }, null, 8 /* PROPS */, [\"contacts\", \"selected-ids\", \"keyword\"])], 64 /* STABLE_FRAGMENT */)) : $setup.activeAction === 'createChannel' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, {\n key: 4\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[27] || (_cache[27] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"频道/公告群名称\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[15] || (_cache[15] = $event => $setup.channelName = $event),\n maxlength: \"64\",\n autocomplete: \"off\",\n placeholder: \"请输入名称\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.channelName, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[28] || (_cache[28] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"公告说明\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"textarea\", {\n \"onUpdate:modelValue\": _cache[16] || (_cache[16] = $event => $setup.channelAnnouncement = $event),\n maxlength: \"1000\",\n rows: \"3\",\n placeholder: \"可选\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.channelAnnouncement, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createVNode)($setup[\"ContactPicker\"], {\n contacts: $setup.filteredContacts,\n \"selected-ids\": $setup.selectedContactIds,\n keyword: $setup.contactKeyword,\n \"empty-text\": \"暂无可选联系人\",\n \"onUpdate:keyword\": _cache[17] || (_cache[17] = $event => $setup.contactKeyword = $event),\n onToggle: $setup.toggleContact\n }, null, 8 /* PROPS */, [\"contacts\", \"selected-ids\", \"keyword\"])], 64 /* STABLE_FRAGMENT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), $setup.modalError ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"p\", _hoisted_15, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.modalError), 1 /* TEXT */)) : $setup.modalInfo ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"p\", _hoisted_16, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.modalInfo), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_17, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"ghost-inline-action\",\n onClick: $setup.closeActionModal\n }, \"取消\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: \"primary-inline-action\",\n disabled: $setup.actionBusy || $setup.submitDisabled\n }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.actionBusy ? '处理中...' : $setup.submitLabel), 9 /* TEXT, PROPS */, _hoisted_18)])], 32 /* NEED_HYDRATION */)])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)]);\n}\n\n//# sourceURL=webpack://jc-web-im/./src/components/sidebar/ConversationList.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B3%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/NavigationRail.vue?vue&type=template&id=021016ee": +/*!************************************************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/NavigationRail.vue?vue&type=template&id=021016ee ***! + \************************************************************************************************************************************************************************************************************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* binding */ render; }\n/* harmony export */ });\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n\nconst _hoisted_1 = {\n class: \"navigation-rail\"\n};\nconst _hoisted_2 = [\"src\"];\nconst _hoisted_3 = {\n key: 1\n};\nconst _hoisted_4 = {\n class: \"settings-panel\"\n};\nconst _hoisted_5 = {\n class: \"settings-layout\"\n};\nconst _hoisted_6 = {\n class: \"settings-menu\",\n \"aria-label\": \"设置菜单\"\n};\nconst _hoisted_7 = [\"onClick\"];\nconst _hoisted_8 = {\n key: 0\n};\nconst _hoisted_9 = {\n key: 0,\n class: \"inline-save-status\"\n};\nconst _hoisted_10 = {\n key: 1,\n class: \"inline-save-status error\"\n};\nconst _hoisted_11 = [\"disabled\"];\nconst _hoisted_12 = {\n key: 1,\n class: \"settings-placeholder\"\n};\nconst _hoisted_13 = {\n class: \"profile-avatar-field\"\n};\nconst _hoisted_14 = [\"src\"];\nconst _hoisted_15 = {\n key: 1\n};\nconst _hoisted_16 = [\"value\"];\nconst _hoisted_17 = {\n class: \"modal-actions\"\n};\nconst _hoisted_18 = {\n key: 0,\n class: \"inline-save-status\"\n};\nconst _hoisted_19 = {\n key: 1,\n class: \"inline-save-status error\"\n};\nconst _hoisted_20 = [\"disabled\"];\nconst _hoisted_21 = {\n class: \"avatar-preview-dialog\"\n};\nconst _hoisted_22 = {\n class: \"avatar-preview-actions\"\n};\nconst _hoisted_23 = [\"src\"];\nconst _hoisted_24 = {\n class: \"modal-panel avatar-cropper-panel\"\n};\nconst _hoisted_25 = {\n class: \"avatar-cropper-header-actions\"\n};\nconst _hoisted_26 = [\"disabled\"];\nconst _hoisted_27 = {\n class: \"avatar-cropper-layout\"\n};\nconst _hoisted_28 = {\n ref: \"cropperCanvas\"\n};\nconst _hoisted_29 = {\n key: 0,\n class: \"avatar-cropper-placeholder\"\n};\nconst _hoisted_30 = {\n class: \"avatar-cropper-controls\"\n};\nconst _hoisted_31 = {\n class: \"avatar-cropper-tools\"\n};\nconst _hoisted_32 = {\n class: \"avatar-cropper-preview\"\n};\nconst _hoisted_33 = {\n ref: \"cropperPreviewCanvas\"\n};\nconst _hoisted_34 = {\n key: 0,\n class: \"inline-save-status error\"\n};\nconst _hoisted_35 = [\"disabled\"];\nconst _hoisted_36 = [\"disabled\"];\nfunction render(_ctx, _cache, $props, $setup, $data, $options) {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, null, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"aside\", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"rail-avatar rail-avatar-button\",\n title: \"编辑个人资料\",\n onClick: $setup.openProfileEditor\n }, [$setup.session.portrait ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"img\", {\n key: 0,\n src: $setup.session.portrait,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_2)) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_3, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.initials), 1 /* TEXT */))]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)([\"rail-button\", {\n active: $setup.navigation.section === 'messages'\n }]),\n title: \"消息\",\n onClick: _cache[0] || (_cache[0] = (...args) => $setup.navigation.showMessages && $setup.navigation.showMessages(...args))\n }, \"💬\", 2 /* CLASS */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)([\"rail-button\", {\n active: $setup.navigation.section === 'contacts'\n }]),\n title: \"联系人\",\n onClick: $setup.showContacts\n }, \"👥\", 2 /* CLASS */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)([\"rail-button\", {\n active: $setup.navigation.section === 'favorites'\n }]),\n title: \"收藏\",\n onClick: _cache[1] || (_cache[1] = (...args) => $setup.navigation.showFavorites && $setup.navigation.showFavorites(...args))\n }, \"☆\", 2 /* CLASS */), _cache[15] || (_cache[15] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n class: \"rail-spacer\"\n }, null, -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: \"rail-button\",\n title: \"设置\",\n \"aria-label\": \"设置\",\n onClick: $setup.openSettings\n }, _cache[13] || (_cache[13] = [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"svg\", {\n viewBox: \"0 0 24 24\",\n \"aria-hidden\": \"true\"\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"path\", {\n d: \"M12 8.2a3.8 3.8 0 1 1 0 7.6 3.8 3.8 0 0 1 0-7.6Z\"\n }), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"path\", {\n d: \"M19.4 13.6a7.8 7.8 0 0 0 .1-1.6l1.7-1.3-1.7-3-2 .8a8 8 0 0 0-1.4-.8L15.8 5h-3.5l-.3 2.1a8 8 0 0 0-1.5.8l-1.9-.8-1.8 3 1.7 1.3a7.8 7.8 0 0 0 0 1.6l-1.7 1.3 1.8 3 1.9-.8c.5.4 1 .7 1.5.8l.3 2.1h3.5l.3-2.1c.5-.2 1-.5 1.4-.8l2 .8 1.7-3-1.8-1.3Z\"\n })], -1 /* HOISTED */)])), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: \"rail-button rail-logout-button\",\n title: \"退出登录\",\n \"aria-label\": \"退出登录\",\n onClick: $setup.logout\n }, _cache[14] || (_cache[14] = [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createStaticVNode)(\"\", 1)]))]), $setup.settingsOpen ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", {\n key: 0,\n class: \"modal-backdrop settings-backdrop\",\n onClick: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.closeSettings, [\"self\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"section\", _hoisted_4, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"header\", null, [_cache[16] || (_cache[16] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, \"设置\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: $setup.closeSettings\n }, \"×\")]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_5, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"nav\", _hoisted_6, [((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(vue__WEBPACK_IMPORTED_MODULE_0__.Fragment, null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.renderList)($setup.settingsItems, item => {\n return (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n key: item.key,\n type: \"button\",\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)({\n active: $setup.settingsSection === item.key\n }),\n onClick: $event => $setup.selectSettingsSection(item.key)\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)(item.label), 1 /* TEXT */), item.key !== 'password' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"small\", _hoisted_8, \"稍后开放\")) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)], 10 /* CLASS, PROPS */, _hoisted_7);\n }), 64 /* STABLE_FRAGMENT */))]), $setup.settingsSection === 'password' ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"form\", {\n key: 0,\n class: \"settings-password-form\",\n onSubmit: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.savePassword, [\"prevent\"])\n }, [_cache[20] || (_cache[20] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"h3\", null, \"更改密码\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[17] || (_cache[17] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"当前密码\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[2] || (_cache[2] = $event => $setup.passwordDraft.oldPassword = $event),\n type: \"password\",\n autocomplete: \"current-password\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.passwordDraft.oldPassword]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[18] || (_cache[18] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"新密码\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[3] || (_cache[3] = $event => $setup.passwordDraft.newPassword = $event),\n type: \"password\",\n autocomplete: \"new-password\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.passwordDraft.newPassword]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[19] || (_cache[19] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"确认新密码\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[4] || (_cache[4] = $event => $setup.passwordDraft.confirmPassword = $event),\n type: \"password\",\n autocomplete: \"new-password\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.passwordDraft.confirmPassword]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"footer\", null, [$setup.settingsNotice ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_9, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.settingsNotice), 1 /* TEXT */)) : $setup.settingsError ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_10, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.settingsError), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"submit\",\n class: \"primary-inline-action\",\n disabled: $setup.passwordSaving\n }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.passwordSaving ? '保存中...' : '保存密码'), 9 /* TEXT, PROPS */, _hoisted_11)])], 32 /* NEED_HYDRATION */)) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"section\", _hoisted_12, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"h3\", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.activeSettingsItem?.label || '设置'), 1 /* TEXT */), _cache[21] || (_cache[21] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"p\", null, \"该功能稍后开放。\", -1 /* HOISTED */))]))])])])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), $setup.profileOpen ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", {\n key: 1,\n class: \"modal-backdrop\",\n onClick: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.closeProfileEditor, [\"self\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"form\", {\n class: \"modal-panel profile-edit-panel\",\n onSubmit: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.saveProfile, [\"prevent\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"header\", null, [_cache[22] || (_cache[22] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, \"个人资料\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: $setup.closeProfileEditor\n }, \"×\")]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_13, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)([\"profile-avatar-preview\", {\n clickable: $setup.currentAvatarUrl\n }]),\n title: \"预览头像\",\n onClick: $setup.openAvatarPreview\n }, [$setup.currentAvatarUrl ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"img\", {\n key: 0,\n src: $setup.currentAvatarUrl,\n alt: \"\"\n }, null, 8 /* PROPS */, _hoisted_14)) : ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_15, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.initials), 1 /* TEXT */))], 2 /* CLASS */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"primary-inline-action profile-avatar-change-button\",\n onClick: $setup.choosePortraitFile\n }, \"更换头像\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n ref: \"portraitInput\",\n type: \"file\",\n accept: \"image/*\",\n onChange: $setup.selectPortrait\n }, null, 544 /* NEED_HYDRATION, NEED_PATCH */)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[23] || (_cache[23] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"昵称\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.withDirectives)((0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n \"onUpdate:modelValue\": _cache[5] || (_cache[5] = $event => $setup.profileDraft.displayName = $event),\n maxlength: \"64\",\n autocomplete: \"off\"\n }, null, 512 /* NEED_PATCH */), [[vue__WEBPACK_IMPORTED_MODULE_0__.vModelText, $setup.profileDraft.displayName, void 0, {\n trim: true\n }]])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"label\", null, [_cache[24] || (_cache[24] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"span\", null, \"用户ID\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"input\", {\n value: $setup.session.userId,\n readonly: \"\"\n }, null, 8 /* PROPS */, _hoisted_16)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_17, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"ghost-inline-action\",\n onClick: $setup.closeProfileEditor\n }, \"取消\"), $setup.profileNotice ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_18, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.profileNotice), 1 /* TEXT */)) : $setup.profileError ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_19, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.profileError), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n class: \"primary-inline-action\",\n disabled: $setup.profileSaving || !$setup.profileDraft.displayName\n }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.profileSaving ? '保存中...' : '保存'), 9 /* TEXT, PROPS */, _hoisted_20)])], 32 /* NEED_HYDRATION */)])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), $setup.avatarPreviewViewer.open ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", {\n key: 2,\n class: \"modal-backdrop avatar-preview-backdrop\",\n onClick: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.closeAvatarPreview, [\"self\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"section\", _hoisted_21, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"header\", null, [_cache[25] || (_cache[25] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, \"头像预览\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_22, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"缩小\",\n onClick: _cache[6] || (_cache[6] = $event => $setup.zoomAvatarPreview(1 / 1.16))\n }, \"−\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"放大\",\n onClick: _cache[7] || (_cache[7] = $event => $setup.zoomAvatarPreview(1.16))\n }, \"+\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"还原\",\n onClick: $setup.resetAvatarPreview\n }, \"1:1\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: $setup.closeAvatarPreview\n }, \"×\")])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)([\"avatar-preview-stage\", {\n dragging: $setup.avatarPreviewViewer.dragging\n }]),\n onWheel: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.handleAvatarPreviewWheel, [\"prevent\"]),\n onPointerdown: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.startAvatarPreviewDrag, [\"prevent\"]),\n onPointermove: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.moveAvatarPreviewDrag, [\"prevent\"]),\n onPointerup: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.stopAvatarPreviewDrag, [\"prevent\"]),\n onPointerleave: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.stopAvatarPreviewDrag, [\"prevent\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"img\", {\n src: $setup.avatarPreviewViewer.url,\n alt: \"头像预览\",\n style: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeStyle)($setup.avatarPreviewStyle),\n draggable: \"false\"\n }, null, 12 /* STYLE, PROPS */, _hoisted_23)], 34 /* CLASS, NEED_HYDRATION */)])])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), $setup.avatarCropper.open ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", {\n key: 3,\n class: \"modal-backdrop avatar-cropper-backdrop\",\n onClick: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.closeAvatarCropper, [\"self\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"section\", _hoisted_24, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"header\", null, [_cache[26] || (_cache[26] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"strong\", null, \"头像预裁切\", -1 /* HOISTED */)), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_25, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"primary-inline-action\",\n disabled: $setup.avatarCropper.busy,\n onClick: $setup.choosePortraitFile\n }, \"上传\", 8 /* PROPS */, _hoisted_26), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"icon-button\",\n title: \"关闭\",\n onClick: $setup.closeAvatarCropper\n }, \"×\")])]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_27, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n class: (0,vue__WEBPACK_IMPORTED_MODULE_0__.normalizeClass)([\"avatar-cropper-stage\", {\n dragging: $setup.avatarCropper.dragging\n }]),\n onWheel: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.handleCropperWheel, [\"prevent\"]),\n onPointerdown: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.startCropperDrag, [\"prevent\"]),\n onPointermove: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.moveCropperDrag, [\"prevent\"]),\n onPointerup: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.stopCropperDrag, [\"prevent\"]),\n onPointerleave: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.stopCropperDrag, [\"prevent\"]),\n onDragover: _cache[8] || (_cache[8] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)(() => {}, [\"prevent\"])),\n onDrop: (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)($setup.dropPortrait, [\"prevent\"])\n }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"canvas\", _hoisted_28, null, 512 /* NEED_PATCH */), _cache[27] || (_cache[27] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n class: \"avatar-cropper-mask\",\n \"aria-hidden\": \"true\"\n }, null, -1 /* HOISTED */)), _cache[28] || (_cache[28] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", {\n class: \"avatar-cropper-box\",\n \"aria-hidden\": \"true\"\n }, null, -1 /* HOISTED */)), !$setup.avatarCropper.loaded ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"div\", _hoisted_29, \"图片加载中\")) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)], 34 /* CLASS, NEED_HYDRATION */), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"aside\", _hoisted_30, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_31, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"左旋90度\",\n onClick: _cache[9] || (_cache[9] = $event => $setup.rotateCropper(-90))\n }, \"↶90°\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"右旋90度\",\n onClick: _cache[10] || (_cache[10] = $event => $setup.rotateCropper(90))\n }, \"↷90°\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"放大\",\n onClick: _cache[11] || (_cache[11] = $event => $setup.zoomCropper(1.12))\n }, \"+\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"缩小\",\n onClick: _cache[12] || (_cache[12] = $event => $setup.zoomCropper(1 / 1.12))\n }, \"−\"), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n title: \"重置\",\n onClick: $setup.resetCropperTransform\n }, \"重置\")]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"div\", _hoisted_32, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"canvas\", _hoisted_33, null, 512 /* NEED_PATCH */)]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"footer\", null, [$setup.avatarCropper.error ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)(\"span\", _hoisted_34, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.avatarCropper.error), 1 /* TEXT */)) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"ghost-inline-action\",\n disabled: $setup.avatarCropper.busy,\n onClick: $setup.closeAvatarCropper\n }, \"取消\", 8 /* PROPS */, _hoisted_35), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)(\"button\", {\n type: \"button\",\n class: \"primary-inline-action\",\n disabled: $setup.avatarCropper.busy || !$setup.avatarCropper.loaded,\n onClick: $setup.confirmAvatarCrop\n }, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($setup.avatarCropper.busy ? '处理中...' : '确认裁切'), 9 /* TEXT, PROPS */, _hoisted_36)])])])])])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)(\"v-if\", true)], 64 /* STABLE_FRAGMENT */);\n}\n\n//# sourceURL=webpack://jc-web-im/./src/components/sidebar/NavigationRail.vue?./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use%5B0%5D!./node_modules/vue-loader/dist/templateLoader.js??ruleSet%5B1%5D.rules%5B3%5D!./node_modules/vue-loader/dist/index.js??ruleSet%5B0%5D.use%5B0%5D\n}"); + +/***/ }), + +/***/ "./src/config.js": +/*!***********************!*\ + !*** ./src/config.js ***! + \***********************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ appConfig: function() { return /* binding */ appConfig; }\n/* harmony export */ });\nconst currentHost = window.location.hostname || '127.0.0.1';\nconst runtimeConfig = window.JC_WEB_IM_CONFIG || {};\nconst appServerOrigin = `${window.location.protocol}//${currentHost}:8888`;\nconst qrAppServerOrigin = `${window.location.protocol}//${currentHost}:18888`;\nconst adminServerOrigin = `${window.location.protocol}//${currentHost}:8024`;\nconst appConfig = {\n appName: 'Web IM',\n adminServer: runtimeConfig.adminServer || undefined || adminServerOrigin,\n appServer: runtimeConfig.appServer || undefined || appServerOrigin,\n qrAppServer: runtimeConfig.qrAppServer || undefined || qrAppServerOrigin,\n platformConfigApi: runtimeConfig.platformConfigApi || undefined || ( true ? '/local-api/platform_config.php' : 0),\n registerConfigApi: runtimeConfig.registerConfigApi || undefined || `${adminServerOrigin}/api/register-config`,\n registerPhoneExistsApi: runtimeConfig.registerPhoneExistsApi || undefined || `${adminServerOrigin}/api/register-phone-exists`,\n contactsApi: runtimeConfig.contactsApi || undefined || ( true ? '/local-api/contacts.php' : 0),\n updateAliasApi: runtimeConfig.updateAliasApi || undefined || ( true ? '/local-api/update_alias.php' : 0),\n addContactApi: runtimeConfig.addContactApi || undefined || ( true ? '/local-api/add_contact.php' : 0),\n conversationActionsApi: runtimeConfig.conversationActionsApi || undefined || ( true ? '/local-api/conversation_actions.php' : 0),\n sendMessageApi: runtimeConfig.sendMessageApi || undefined || ( true ? '/local-api/send_message.php' : 0),\n messageActionApi: runtimeConfig.messageActionApi || undefined || ( true ? '/local-api/message_action.php' : 0),\n messageDetailApi: runtimeConfig.messageDetailApi || undefined || ( true ? '/local-api/message_detail.php' : 0),\n messagesApi: runtimeConfig.messagesApi || undefined || ( true ? '/local-api/messages.php' : 0),\n profileApi: runtimeConfig.profileApi || undefined || ( true ? '/local-api/profile.php' : 0),\n changePasswordApi: runtimeConfig.changePasswordApi || undefined || `${appServerOrigin}/auth/changePassword`,\n logoutApi: runtimeConfig.logoutApi || undefined || `${appServerOrigin}/auth/logout`,\n imServerHost: runtimeConfig.imServerHost || undefined || currentHost,\n imRoutePort: Number(runtimeConfig.imRoutePort || undefined || 18000),\n useWss: runtimeConfig.useWss ?? undefined === 'true',\n wsPath: runtimeConfig.wsPath || undefined || '/ws',\n wsUrl: runtimeConfig.wsUrl || undefined || '',\n sdkMode: runtimeConfig.sdkMode || undefined || 'http',\n syncIntervalMs: Number(runtimeConfig.syncIntervalMs || undefined || 300),\n sseEnabled: runtimeConfig.sseEnabled ?? undefined === 'true',\n sseTicketApi: runtimeConfig.sseTicketApi || undefined || '/api/sse_ticket.php',\n sseUrl: runtimeConfig.sseUrl || undefined || '/ws-events',\n platform: 5,\n clientIdStorageKey: 'qmt-web-im-client-id',\n authTokenStoragePrefix: 'qmt-web-im-auth-token-'\n};\n\n//# sourceURL=webpack://jc-web-im/./src/config.js?\n}"); + +/***/ }), + +/***/ "./src/main.js": +/*!*********************!*\ + !*** ./src/main.js ***! + \*********************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! vue */ \"./node_modules/vue/dist/vue.runtime.esm-bundler.js\");\n/* harmony import */ var pinia__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! pinia */ \"./node_modules/pinia/dist/pinia.mjs\");\n/* harmony import */ var _App_vue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./App.vue */ \"./src/App.vue\");\n/* harmony import */ var _styles_base_css__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./styles/base.css */ \"./src/styles/base.css\");\n/* harmony import */ var _styles_base_css__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(_styles_base_css__WEBPACK_IMPORTED_MODULE_3__);\n\n\n\n\n(0,vue__WEBPACK_IMPORTED_MODULE_0__.createApp)(_App_vue__WEBPACK_IMPORTED_MODULE_2__[\"default\"]).use((0,pinia__WEBPACK_IMPORTED_MODULE_1__.createPinia)()).mount('#app');\n\n//# sourceURL=webpack://jc-web-im/./src/main.js?\n}"); + +/***/ }), + +/***/ "./src/services/appServerClient.js": +/*!*****************************************!*\ + !*** ./src/services/appServerClient.js ***! + \*****************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ appServerGet: function() { return /* binding */ appServerGet; },\n/* harmony export */ appServerPost: function() { return /* binding */ appServerPost; },\n/* harmony export */ appServerRequest: function() { return /* binding */ appServerRequest; },\n/* harmony export */ clearStoredAppAuth: function() { return /* binding */ clearStoredAppAuth; },\n/* harmony export */ getStoredAppAuth: function() { return /* binding */ getStoredAppAuth; },\n/* harmony export */ persistAppAuth: function() { return /* binding */ persistAppAuth; }\n/* harmony export */ });\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.iterator.constructor.js */ \"./node_modules/core-js/modules/es.iterator.constructor.js\");\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.iterator.for-each.js */ \"./node_modules/core-js/modules/es.iterator.for-each.js\");\n/* harmony import */ var core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var core_js_modules_es_typed_array_with_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/es.typed-array.with.js */ \"./node_modules/core-js/modules/es.typed-array.with.js\");\n/* harmony import */ var core_js_modules_es_typed_array_with_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_typed_array_with_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var core_js_modules_es_uint8_array_set_from_base64_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! core-js/modules/es.uint8-array.set-from-base64.js */ \"./node_modules/core-js/modules/es.uint8-array.set-from-base64.js\");\n/* harmony import */ var core_js_modules_es_uint8_array_set_from_base64_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_uint8_array_set_from_base64_js__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var core_js_modules_es_uint8_array_set_from_hex_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! core-js/modules/es.uint8-array.set-from-hex.js */ \"./node_modules/core-js/modules/es.uint8-array.set-from-hex.js\");\n/* harmony import */ var core_js_modules_es_uint8_array_set_from_hex_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_uint8_array_set_from_hex_js__WEBPACK_IMPORTED_MODULE_4__);\n/* harmony import */ var core_js_modules_es_uint8_array_to_base64_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! core-js/modules/es.uint8-array.to-base64.js */ \"./node_modules/core-js/modules/es.uint8-array.to-base64.js\");\n/* harmony import */ var core_js_modules_es_uint8_array_to_base64_js__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_uint8_array_to_base64_js__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var core_js_modules_es_uint8_array_to_hex_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! core-js/modules/es.uint8-array.to-hex.js */ \"./node_modules/core-js/modules/es.uint8-array.to-hex.js\");\n/* harmony import */ var core_js_modules_es_uint8_array_to_hex_js__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_uint8_array_to_hex_js__WEBPACK_IMPORTED_MODULE_6__);\n/* harmony import */ var core_js_modules_web_dom_exception_stack_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! core-js/modules/web.dom-exception.stack.js */ \"./node_modules/core-js/modules/web.dom-exception.stack.js\");\n/* harmony import */ var core_js_modules_web_dom_exception_stack_js__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_web_dom_exception_stack_js__WEBPACK_IMPORTED_MODULE_7__);\n/* harmony import */ var axios__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! axios */ \"./node_modules/axios/lib/axios.js\");\n/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! @/config */ \"./src/config.js\");\n\n\n\n\n\n\n\n\n\n\nconst AES_KEY = 'xczx2026!@#secKe';\nconst ENCRYPTION_EXCLUDE_PATHS = ['/avatar/', '/f/', '/amr2mp3', '/appVersion/check'];\nconst AUTH_STORAGE_KEY = 'qmt-web-im-app-auth-v1';\nconst AUTH_ERROR_CODE = 13;\nconst REFRESH_ERROR_CODE = 20045;\nlet importedCryptoKeyPromise = null;\nlet refreshPromise = null;\nfunction storageScope() {\n return _config__WEBPACK_IMPORTED_MODULE_9__.appConfig.appServer || '';\n}\nfunction readAuthStore() {\n try {\n return JSON.parse(localStorage.getItem(AUTH_STORAGE_KEY) || '{}');\n } catch (error) {\n return {};\n }\n}\nfunction writeAuthStore(store) {\n localStorage.setItem(AUTH_STORAGE_KEY, JSON.stringify(store));\n}\nfunction normalizeAuthBundle(bundle = {}) {\n const accessToken = String(bundle.accessToken || '').trim();\n const refreshToken = String(bundle.refreshToken || '').trim();\n if (!accessToken || !refreshToken) {\n return null;\n }\n return {\n accessToken,\n refreshToken,\n tokenType: String(bundle.tokenType || 'Bearer').trim() || 'Bearer',\n accessTokenExpireIn: Number(bundle.accessTokenExpireIn || 0) || 0,\n accessTokenExpireAt: Number(bundle.accessTokenExpireAt || 0) || 0,\n refreshTokenExpireIn: Number(bundle.refreshTokenExpireIn || 0) || 0,\n refreshTokenExpireAt: Number(bundle.refreshTokenExpireAt || 0) || 0\n };\n}\nfunction getStoredAppAuth() {\n const store = readAuthStore();\n return normalizeAuthBundle(store[storageScope()]);\n}\nfunction persistAppAuth(bundle) {\n const normalized = normalizeAuthBundle(bundle);\n const store = readAuthStore();\n if (!normalized) {\n delete store[storageScope()];\n writeAuthStore(store);\n return null;\n }\n store[storageScope()] = normalized;\n writeAuthStore(store);\n return normalized;\n}\nfunction clearStoredAppAuth() {\n persistAppAuth(null);\n}\nfunction hasFormData(value) {\n return typeof FormData !== 'undefined' && value instanceof FormData;\n}\nfunction absoluteUrl(url) {\n return new URL(url, window.location.origin);\n}\nfunction shouldEncrypt(url) {\n const pathname = absoluteUrl(url).pathname;\n return !ENCRYPTION_EXCLUDE_PATHS.some(path => pathname === path || pathname.startsWith(path));\n}\nfunction shouldAttachAuth(config) {\n return !config.skipAuth;\n}\nfunction textEncoder() {\n return new TextEncoder();\n}\nfunction textDecoder() {\n return new TextDecoder();\n}\nfunction bytesToBase64(bytes) {\n let binary = '';\n bytes.forEach(byte => {\n binary += String.fromCharCode(byte);\n });\n return btoa(binary);\n}\nfunction base64ToBytes(base64) {\n const binary = atob(base64);\n const bytes = new Uint8Array(binary.length);\n for (let index = 0; index < binary.length; index += 1) {\n bytes[index] = binary.charCodeAt(index);\n }\n return bytes;\n}\nasync function getCryptoKey() {\n if (!importedCryptoKeyPromise) {\n importedCryptoKeyPromise = window.crypto.subtle.importKey('raw', textEncoder().encode(AES_KEY), {\n name: 'AES-CBC'\n }, false, ['encrypt', 'decrypt']);\n }\n return importedCryptoKeyPromise;\n}\nasync function encryptPayload(plainText) {\n const iv = window.crypto.getRandomValues(new Uint8Array(16));\n const key = await getCryptoKey();\n const encrypted = await window.crypto.subtle.encrypt({\n name: 'AES-CBC',\n iv\n }, key, textEncoder().encode(plainText));\n return `${bytesToBase64(iv)}:${bytesToBase64(new Uint8Array(encrypted))}`;\n}\nasync function decryptPayload(cipherText) {\n const [ivBase64, encryptedBase64] = String(cipherText || '').split(':', 2);\n if (!ivBase64 || !encryptedBase64) {\n throw new Error('Encrypted payload format is invalid');\n }\n const key = await getCryptoKey();\n const decrypted = await window.crypto.subtle.decrypt({\n name: 'AES-CBC',\n iv: base64ToBytes(ivBase64)\n }, key, base64ToBytes(encryptedBase64));\n return textDecoder().decode(decrypted);\n}\nfunction normalizeBusinessResponse(payload) {\n if (payload && typeof payload === 'object' && !Array.isArray(payload)) {\n return payload;\n }\n return {\n code: 0,\n message: 'success',\n data: payload\n };\n}\nasync function decodeResponsePayload(response) {\n const encrypted = String(response.headers?.['x-encrypted'] || '').toLowerCase() === 'true';\n if (encrypted && typeof response.data === 'string') {\n const plainText = await decryptPayload(response.data);\n response.data = normalizeBusinessResponse(JSON.parse(plainText));\n return response;\n }\n response.data = normalizeBusinessResponse(response.data);\n return response;\n}\nconst client = axios__WEBPACK_IMPORTED_MODULE_8__[\"default\"].create({\n withCredentials: false\n});\nclient.interceptors.request.use(async config => {\n const nextConfig = {\n ...config\n };\n const url = absoluteUrl(nextConfig.url || '').toString();\n if (shouldAttachAuth(nextConfig)) {\n const auth = getStoredAppAuth();\n if (auth?.accessToken) {\n nextConfig.headers = {\n ...(nextConfig.headers || {}),\n Authorization: `${auth.tokenType || 'Bearer'} ${auth.accessToken}`\n };\n }\n }\n if (!hasFormData(nextConfig.data) && nextConfig.method?.toLowerCase() !== 'get' && shouldEncrypt(url)) {\n const plainBody = JSON.stringify(nextConfig.data ?? {});\n nextConfig.data = await encryptPayload(plainBody);\n nextConfig.headers = {\n ...(nextConfig.headers || {}),\n 'Content-Type': 'text/plain;charset=UTF-8',\n 'X-Timestamp': `${Math.floor(Date.now() / 1000)}`\n };\n }\n return nextConfig;\n});\nclient.interceptors.response.use(async response => decodeResponsePayload(response));\nasync function refreshAccessToken() {\n if (refreshPromise) {\n return refreshPromise;\n }\n const auth = getStoredAppAuth();\n if (!auth?.refreshToken) {\n clearStoredAppAuth();\n throw new Error('登录状态已失效,请重新登录');\n }\n refreshPromise = client.post(`${_config__WEBPACK_IMPORTED_MODULE_9__.appConfig.appServer}/auth/refreshToken`, {\n refreshToken: auth.refreshToken\n }, {\n skipAuth: true\n }).then(response => {\n const payload = response.data || {};\n if (Number(payload.code) !== 0 || !payload.data) {\n if (Number(payload.code) === REFRESH_ERROR_CODE) {\n clearStoredAppAuth();\n }\n throw new Error(payload.message || '刷新登录状态失败');\n }\n persistAppAuth(payload.data);\n return payload.data;\n }).finally(() => {\n refreshPromise = null;\n });\n return refreshPromise;\n}\nasync function request(config) {\n const response = await client.request(config);\n const payload = response.data || {};\n if (Number(payload.code) === AUTH_ERROR_CODE && shouldAttachAuth(config) && !config._retriedAfterRefresh) {\n await refreshAccessToken();\n return request({\n ...config,\n _retriedAfterRefresh: true\n });\n }\n return response;\n}\nasync function appServerRequest(config) {\n return request(config);\n}\nasync function appServerGet(url, config = {}) {\n return request({\n ...config,\n method: 'get',\n url\n });\n}\nasync function appServerPost(url, data = {}, config = {}) {\n return request({\n ...config,\n method: 'post',\n url,\n data\n });\n}\n\n//# sourceURL=webpack://jc-web-im/./src/services/appServerClient.js?\n}"); + +/***/ }), + +/***/ "./src/services/documentedHttpImClient.js": +/*!************************************************!*\ + !*** ./src/services/documentedHttpImClient.js ***! + \************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ DocumentedHttpImClient: function() { return /* binding */ DocumentedHttpImClient; }\n/* harmony export */ });\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.includes.js */ \"./node_modules/core-js/modules/es.array.includes.js\");\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @/config */ \"./src/config.js\");\n/* harmony import */ var _httpImClient__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./httpImClient */ \"./src/services/httpImClient.js\");\n/* harmony import */ var axios__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! axios */ \"./node_modules/axios/lib/axios.js\");\n/* harmony import */ var _appServerClient__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./appServerClient */ \"./src/services/appServerClient.js\");\n\n\n\n\n\nfunction inferAccountType(account) {\n return String(account || '').includes('@') ? 2 : 1;\n}\nfunction unwrapData(response) {\n return response.data?.data ?? response.data?.result ?? null;\n}\nfunction unwrapMessage(response, fallback) {\n return response.data?.message || response.data?.msg || fallback;\n}\nclass DocumentedHttpImClient extends _httpImClient__WEBPACK_IMPORTED_MODULE_2__.HttpImClient {\n unsupported(feature) {\n throw new Error(`当前对接文档未提供“${feature}”接口,已禁用旧本地代理链路`);\n }\n async loginWithPassword(account, password, type = inferAccountType(account)) {\n const response = await (0,_appServerClient__WEBPACK_IMPORTED_MODULE_4__.appServerPost)(`${_config__WEBPACK_IMPORTED_MODULE_1__.appConfig.appServer}/auth/login_pwd`, {\n type,\n number: account,\n password,\n platform: _config__WEBPACK_IMPORTED_MODULE_1__.appConfig.platform,\n clientId: this.getClientId()\n }, {\n skipAuth: true\n });\n if (response.data?.code !== 0) {\n throw new Error(unwrapMessage(response, '登录失败'));\n }\n (0,_appServerClient__WEBPACK_IMPORTED_MODULE_4__.persistAppAuth)(unwrapData(response));\n return unwrapData(response);\n }\n async requestAuthCode(email) {\n const response = await (0,_appServerClient__WEBPACK_IMPORTED_MODULE_4__.appServerPost)(`${_config__WEBPACK_IMPORTED_MODULE_1__.appConfig.appServer}/auth/sendEmailCode`, {\n email\n }, {\n skipAuth: true\n });\n if (response.data?.code !== 0) {\n throw new Error(unwrapMessage(response, '验证码发送失败'));\n }\n return unwrapData(response);\n }\n async loginWithCode(account, code) {\n void account;\n void code;\n throw new Error('接口文档未提供验证码登录接口,请使用密码登录');\n }\n async register({\n type,\n number,\n code = '',\n password,\n invitationCode = ''\n }) {\n const response = await (0,_appServerClient__WEBPACK_IMPORTED_MODULE_4__.appServerPost)(`${_config__WEBPACK_IMPORTED_MODULE_1__.appConfig.appServer}/auth/register`, {\n type,\n number,\n code,\n password,\n clientId: this.getClientId(),\n platform: _config__WEBPACK_IMPORTED_MODULE_1__.appConfig.platform,\n invitationCode\n }, {\n skipAuth: true\n });\n if (response.data?.code !== 0) {\n throw new Error(unwrapMessage(response, '注册失败'));\n }\n (0,_appServerClient__WEBPACK_IMPORTED_MODULE_4__.persistAppAuth)(unwrapData(response));\n return unwrapData(response);\n }\n async createPcSession(userId = '') {\n const response = await axios__WEBPACK_IMPORTED_MODULE_3__[\"default\"].post(`${_config__WEBPACK_IMPORTED_MODULE_1__.appConfig.qrAppServer}/pc_session`, {\n flag: 1,\n device_name: 'Web IM',\n userId,\n clientId: this.getClientId(),\n platform: _config__WEBPACK_IMPORTED_MODULE_1__.appConfig.platform\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(unwrapMessage(response, '二维码登录会话创建失败'));\n }\n return unwrapData(response) || {};\n }\n async loginWithPcSession(token) {\n const url = `${_config__WEBPACK_IMPORTED_MODULE_1__.appConfig.qrAppServer}/session_login/${encodeURIComponent(token)}`;\n let response;\n try {\n response = await axios__WEBPACK_IMPORTED_MODULE_3__[\"default\"].post(url, null, {\n withCredentials: false\n });\n } catch (postError) {\n response = await axios__WEBPACK_IMPORTED_MODULE_3__[\"default\"].get(url, {\n withCredentials: false\n });\n }\n this.persistAppToken(url, response);\n const payload = response.data || {};\n const result = payload.result ?? payload.data ?? null;\n if (Number(payload.code) === 0 && result) {\n (0,_appServerClient__WEBPACK_IMPORTED_MODULE_4__.persistAppAuth)(result);\n }\n if ([0, 8, 9, 10, 13, 18, 20042].includes(Number(payload.code))) {\n return {\n ...payload,\n data: result\n };\n }\n throw new Error(payload.message || '扫码登录失败');\n }\n async changePassword({\n oldPassword,\n newPassword\n }) {\n const response = await (0,_appServerClient__WEBPACK_IMPORTED_MODULE_4__.appServerPost)(_config__WEBPACK_IMPORTED_MODULE_1__.appConfig.changePasswordApi, {\n oldPassword,\n newPassword\n });\n if (response.data?.code !== 0) {\n throw new Error(unwrapMessage(response, '密码修改失败'));\n }\n return true;\n }\n async resetPasswordWithCode({\n mobile,\n resetCode,\n newPassword\n }) {\n void mobile;\n void resetCode;\n void newPassword;\n throw new Error('接口文档未提供重置密码接口,请联系服务端确认流程');\n }\n async logout() {\n const auth = (0,_appServerClient__WEBPACK_IMPORTED_MODULE_4__.getStoredAppAuth)();\n if (!auth?.refreshToken) {\n (0,_appServerClient__WEBPACK_IMPORTED_MODULE_4__.clearStoredAppAuth)();\n return true;\n }\n try {\n const response = await (0,_appServerClient__WEBPACK_IMPORTED_MODULE_4__.appServerPost)(_config__WEBPACK_IMPORTED_MODULE_1__.appConfig.logoutApi, {\n refreshToken: auth.refreshToken\n });\n if (response.data?.code !== 0) {\n throw new Error(unwrapMessage(response, '退出登录失败'));\n }\n } finally {\n (0,_appServerClient__WEBPACK_IMPORTED_MODULE_4__.clearStoredAppAuth)();\n }\n return true;\n }\n async fetchRemoteConversations() {\n return [];\n }\n async fetchRemoteMessages() {\n return [];\n }\n async getServerContactList() {\n return [];\n }\n async getProfile() {\n return null;\n }\n async sendTextMessage() {\n this.unsupported('发送消息');\n }\n async sendAttachmentMessage() {\n this.unsupported('发送附件消息');\n }\n async addContact() {\n this.unsupported('添加联系人');\n }\n async updateContactAlias() {\n this.unsupported('联系人备注');\n }\n async createGroup() {\n this.unsupported('创建群组');\n }\n async joinGroup() {\n this.unsupported('加入群组');\n }\n async broadcastMessage() {\n this.unsupported('群发消息');\n }\n async getGroupDetail() {\n this.unsupported('群详情');\n }\n async getGroupMembers() {\n this.unsupported('群成员');\n }\n async updateGroupAlias() {\n this.unsupported('群备注');\n }\n async leaveGroup() {\n this.unsupported('退出群组');\n }\n async dismissGroup() {\n this.unsupported('解散群组');\n }\n async transferGroup() {\n this.unsupported('转让群组');\n }\n async setGroupAdmins() {\n this.unsupported('设置群管理员');\n }\n async updateGroupProfile() {\n this.unsupported('更新群资料');\n }\n async kickGroupMembers() {\n this.unsupported('移除群成员');\n }\n async updateGroupMuteSetting() {\n this.unsupported('群禁言设置');\n }\n async updateProfile() {\n this.unsupported('更新个人资料');\n }\n async recallMessage() {\n this.unsupported('撤回消息');\n }\n async editMessage() {\n this.unsupported('编辑消息');\n }\n async getMessageDetail() {\n this.unsupported('消息详情');\n }\n}\n\n//# sourceURL=webpack://jc-web-im/./src/services/documentedHttpImClient.js?\n}"); + +/***/ }), + +/***/ "./src/services/httpImClient.js": +/*!**************************************!*\ + !*** ./src/services/httpImClient.js ***! + \**************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ HttpImClient: function() { return /* binding */ HttpImClient; },\n/* harmony export */ imClient: function() { return /* binding */ imClient; }\n/* harmony export */ });\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.includes.js */ \"./node_modules/core-js/modules/es.array.includes.js\");\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.array.push.js */ \"./node_modules/core-js/modules/es.array.push.js\");\n/* harmony import */ var core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/es.iterator.constructor.js */ \"./node_modules/core-js/modules/es.iterator.constructor.js\");\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! core-js/modules/es.iterator.filter.js */ \"./node_modules/core-js/modules/es.iterator.filter.js\");\n/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! core-js/modules/es.iterator.find.js */ \"./node_modules/core-js/modules/es.iterator.find.js\");\n/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_4__);\n/* harmony import */ var core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! core-js/modules/es.iterator.for-each.js */ \"./node_modules/core-js/modules/es.iterator.for-each.js\");\n/* harmony import */ var core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! core-js/modules/es.iterator.map.js */ \"./node_modules/core-js/modules/es.iterator.map.js\");\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_6__);\n/* harmony import */ var core_js_modules_es_iterator_some_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! core-js/modules/es.iterator.some.js */ \"./node_modules/core-js/modules/es.iterator.some.js\");\n/* harmony import */ var core_js_modules_es_iterator_some_js__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_some_js__WEBPACK_IMPORTED_MODULE_7__);\n/* harmony import */ var core_js_modules_es_map_get_or_insert_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! core-js/modules/es.map.get-or-insert.js */ \"./node_modules/core-js/modules/es.map.get-or-insert.js\");\n/* harmony import */ var core_js_modules_es_map_get_or_insert_js__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_map_get_or_insert_js__WEBPACK_IMPORTED_MODULE_8__);\n/* harmony import */ var core_js_modules_es_map_get_or_insert_computed_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! core-js/modules/es.map.get-or-insert-computed.js */ \"./node_modules/core-js/modules/es.map.get-or-insert-computed.js\");\n/* harmony import */ var core_js_modules_es_map_get_or_insert_computed_js__WEBPACK_IMPORTED_MODULE_9___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_map_get_or_insert_computed_js__WEBPACK_IMPORTED_MODULE_9__);\n/* harmony import */ var core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! core-js/modules/es.set.difference.v2.js */ \"./node_modules/core-js/modules/es.set.difference.v2.js\");\n/* harmony import */ var core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_10___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_10__);\n/* harmony import */ var core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! core-js/modules/es.set.intersection.v2.js */ \"./node_modules/core-js/modules/es.set.intersection.v2.js\");\n/* harmony import */ var core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_11___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_11__);\n/* harmony import */ var core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! core-js/modules/es.set.is-disjoint-from.v2.js */ \"./node_modules/core-js/modules/es.set.is-disjoint-from.v2.js\");\n/* harmony import */ var core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_12___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_12__);\n/* harmony import */ var core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! core-js/modules/es.set.is-subset-of.v2.js */ \"./node_modules/core-js/modules/es.set.is-subset-of.v2.js\");\n/* harmony import */ var core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_13___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_13__);\n/* harmony import */ var core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(/*! core-js/modules/es.set.is-superset-of.v2.js */ \"./node_modules/core-js/modules/es.set.is-superset-of.v2.js\");\n/* harmony import */ var core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_14___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_14__);\n/* harmony import */ var core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(/*! core-js/modules/es.set.symmetric-difference.v2.js */ \"./node_modules/core-js/modules/es.set.symmetric-difference.v2.js\");\n/* harmony import */ var core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_15___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_15__);\n/* harmony import */ var core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(/*! core-js/modules/es.set.union.v2.js */ \"./node_modules/core-js/modules/es.set.union.v2.js\");\n/* harmony import */ var core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_16___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_16__);\n/* harmony import */ var axios__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(/*! axios */ \"./node_modules/axios/lib/axios.js\");\n/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(/*! @/config */ \"./src/config.js\");\n/* harmony import */ var _services_appServerClient__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(/*! @/services/appServerClient */ \"./src/services/appServerClient.js\");\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nconst LOCAL_MESSAGE_STORAGE_KEY = 'qmt-web-im-local-messages-v2';\nconst LOCAL_HIDDEN_MESSAGE_STORAGE_KEY = 'qmt-web-im-hidden-messages-v1';\nconst LOCAL_RECALLED_MESSAGE_STORAGE_KEY = 'qmt-web-im-recalled-messages-v1';\nfunction scopedStorageKey(baseKey, userId = '') {\n const scope = `${_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.appServer || ''}:${userId || 'anonymous'}`;\n return `${baseKey}:${encodeURIComponent(scope)}`;\n}\nfunction randomId() {\n if (window.crypto && window.crypto.randomUUID) {\n return window.crypto.randomUUID().replaceAll('-', '');\n }\n return `${Date.now()}${Math.random().toString(16).slice(2)}`;\n}\nfunction readLocalMessages(userId) {\n try {\n return JSON.parse(localStorage.getItem(scopedStorageKey(LOCAL_MESSAGE_STORAGE_KEY, userId)) || '{}');\n } catch (error) {\n return {};\n }\n}\nfunction readMessageSet(storageKey, userId) {\n try {\n return JSON.parse(localStorage.getItem(scopedStorageKey(storageKey, userId)) || '{}');\n } catch (error) {\n return {};\n }\n}\nfunction writeMessageSet(storageKey, messagesByConversation, userId) {\n localStorage.setItem(scopedStorageKey(storageKey, userId), JSON.stringify(messagesByConversation));\n}\nfunction messageKeys(message) {\n return [message?.id, message?.messageUid].filter(Boolean).map(value => String(value));\n}\nfunction hasStoredMessage(storageKey, conversationId, message, userId) {\n const stored = readMessageSet(storageKey, userId);\n const ids = new Set(stored[conversationId] || []);\n return messageKeys(message).some(id => ids.has(id));\n}\nfunction storeMessageKeys(storageKey, conversationId, messages, userId) {\n const stored = readMessageSet(storageKey, userId);\n const ids = new Set(stored[conversationId] || []);\n messages.forEach(message => {\n messageKeys(message).forEach(id => ids.add(id));\n });\n stored[conversationId] = Array.from(ids);\n writeMessageSet(storageKey, stored, userId);\n}\nfunction withRecalledState(message) {\n return {\n ...message,\n status: 'recalled',\n deliveryStatus: '',\n recalled: true,\n recallStatus: '已撤回'\n };\n}\nfunction applyLocalMessageState(conversationId, messages, userId) {\n return messages.filter(message => !hasStoredMessage(LOCAL_HIDDEN_MESSAGE_STORAGE_KEY, conversationId, message, userId)).map(message => {\n const isRecalled = Boolean(message.recalled) || message.status === 'recalled' || hasStoredMessage(LOCAL_RECALLED_MESSAGE_STORAGE_KEY, conversationId, message, userId);\n if (!isRecalled) {\n return message;\n }\n return withRecalledState(message);\n });\n}\nfunction writeLocalMessages(messagesByConversation, userId) {\n localStorage.setItem(scopedStorageKey(LOCAL_MESSAGE_STORAGE_KEY, userId), JSON.stringify(messagesByConversation));\n}\nfunction saveLocalMessage(message, userId) {\n const messagesByConversation = readLocalMessages(userId);\n messagesByConversation[message.conversationId] = messagesByConversation[message.conversationId] || [];\n const index = messagesByConversation[message.conversationId].findIndex(item => item.id === message.id);\n if (index >= 0) {\n messagesByConversation[message.conversationId].splice(index, 1, message);\n } else {\n messagesByConversation[message.conversationId].push(message);\n }\n writeLocalMessages(messagesByConversation, userId);\n}\nfunction mergeSyncedMessages(localMessages, remoteMessages, conversationId, userId) {\n const visibleLocalMessages = applyLocalMessageState(conversationId, localMessages, userId);\n const localByKey = new Map();\n visibleLocalMessages.forEach(message => {\n messageKeys(message).forEach(key => {\n if (!localByKey.has(key)) {\n localByKey.set(key, message);\n }\n });\n });\n const visibleRemoteMessages = applyLocalMessageState(conversationId, remoteMessages, userId).map(remoteMessage => {\n if (!remoteMessage.recalled || String(remoteMessage.text || '').trim()) {\n return remoteMessage;\n }\n const localMessage = messageKeys(remoteMessage).map(key => localByKey.get(key)).find(message => message?.text && message.text !== '你撤回了一条消息');\n return localMessage ? {\n ...remoteMessage,\n text: localMessage.text\n } : remoteMessage;\n });\n const remoteKeys = new Set();\n visibleRemoteMessages.forEach(message => {\n messageKeys(message).forEach(key => remoteKeys.add(key));\n });\n const now = Date.now();\n function hasRemoteMatch(localMessage) {\n if (localMessage.messageUid && remoteKeys.has(localMessage.messageUid)) {\n return true;\n }\n return visibleRemoteMessages.some(remoteMessage => {\n return remoteMessage.direction === localMessage.direction && remoteMessage.text === localMessage.text && Math.abs(Number(remoteMessage.timestamp || 0) - Number(localMessage.timestamp || 0)) < 15000;\n });\n }\n const pendingLocalMessages = visibleLocalMessages.filter(message => {\n if (message.status === 'recalled' || message.recalled) {\n return message.direction === 'out' && !hasRemoteMatch(message);\n }\n if (message.direction !== 'out') {\n return false;\n }\n if (message.status === 'sent' && message.messageUid) {\n return !hasRemoteMatch(message);\n }\n if (message.status === 'sent') {\n return !hasRemoteMatch(message) && now - Number(message.timestamp || 0) < 30000;\n }\n return message.status === 'sending' || message.status === 'failed';\n });\n return [...visibleRemoteMessages, ...pendingLocalMessages].sort((a, b) => {\n return Number(a.displayTimestamp || a.timestamp || 0) - Number(b.displayTimestamp || b.timestamp || 0);\n });\n}\nfunction getTitle(value, fallback) {\n return value || fallback || '';\n}\nfunction getUniqueEquipment(mobile, fallback) {\n const allowedDevices = {\n '18301902119': '67072d37062c4f31b1165073552933ba',\n '18301902116': '1777479418113eeea8e236edce',\n '18301902118': 'de89fb2f2a61480a8c381f6c16e28bd8',\n '18301902117': 'd117f480faa64145b6162107b1b49617'\n };\n return allowedDevices[mobile] || localStorage.getItem('uniqueEquipment') || fallback;\n}\nfunction canUseLocalLoginFallback(response) {\n const message = response.data?.message || '';\n return true && message.includes('允许此设备登录');\n}\nclass HttpImClient {\n constructor() {\n this.listeners = new Map();\n this.connected = false;\n this.currentUser = null;\n }\n getClientId() {\n let clientId = localStorage.getItem(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.clientIdStorageKey);\n if (!clientId) {\n clientId = randomId();\n localStorage.setItem(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.clientIdStorageKey, clientId);\n }\n return clientId;\n }\n restoreClientId(clientId) {\n if (clientId) {\n localStorage.setItem(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.clientIdStorageKey, clientId);\n }\n }\n getSdkVersion() {\n return 'HTTP';\n }\n async ensureSdk() {\n return true;\n }\n async loginWithPassword(mobile, password) {\n const url = `${_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.appServer}/login_pwd`;\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(url, {\n mobile,\n password,\n platform: _config__WEBPACK_IMPORTED_MODULE_18__.appConfig.platform,\n clientId: this.getClientId()\n }, {\n withCredentials: false\n });\n this.persistAppToken(url, response);\n if (response.data?.code !== 0) {\n if (canUseLocalLoginFallback(response)) {\n return {\n userId: mobile,\n token: `local-device-pending-${Date.now()}`,\n displayName: mobile,\n localOnly: true\n };\n }\n throw new Error(response.data?.message || '登录失败');\n }\n return response.data.result;\n }\n async requestAuthCode(mobile) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(`${_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.appServer}/send_code`, {\n mobile\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '验证码发送失败');\n }\n return response.data.result;\n }\n async loginWithCode(mobile, code) {\n const url = `${_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.appServer}/login`;\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(url, {\n mobile,\n code,\n platform: _config__WEBPACK_IMPORTED_MODULE_18__.appConfig.platform,\n clientId: this.getClientId()\n }, {\n withCredentials: false\n });\n this.persistAppToken(url, response);\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '登录失败');\n }\n return response.data.result;\n }\n async createPcSession(userId = '') {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(`${_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.appServer}/pc_session`, {\n flag: 1,\n device_name: 'Web IM',\n userId,\n clientId: this.getClientId(),\n platform: _config__WEBPACK_IMPORTED_MODULE_18__.appConfig.platform\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '二维码登录会话创建失败');\n }\n return response.data.result || {};\n }\n async loginWithPcSession(token) {\n const url = `${_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.appServer}/session_login/${encodeURIComponent(token)}`;\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(url, null, {\n withCredentials: false\n });\n this.persistAppToken(url, response);\n const data = response.data || {};\n if ([0, 8, 9, 10, 18].includes(Number(data.code))) {\n return data;\n }\n throw new Error(data.message || '扫码登录失败');\n }\n async registerWithPhone(mobile, code) {\n const url = `${_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.appServer}/register`;\n const deviceId = getUniqueEquipment(mobile, this.getClientId());\n try {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(url, {\n mobile,\n code,\n platform: _config__WEBPACK_IMPORTED_MODULE_18__.appConfig.platform,\n clientId: deviceId,\n uniqueEquipment: deviceId\n }, {\n withCredentials: false\n });\n this.persistAppToken(url, response);\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '注册失败');\n }\n return response.data.result;\n } catch (error) {\n const status = error.response?.status;\n if (status === 404 || status === 405) {\n return this.loginWithCode(mobile, code);\n }\n throw error;\n }\n }\n connect(userId, token) {\n this.currentUser = {\n userId,\n token\n };\n this.connected = true;\n this.emit('connectionStatusChanged', {\n connected: true,\n statusText: '已连接'\n });\n this.refreshConversationList();\n }\n disconnect() {\n this.connected = false;\n this.currentUser = null;\n this.emit('connectionStatusChanged', {\n connected: false,\n statusText: '未连接'\n });\n }\n getConversationList() {\n return [];\n }\n refreshConversationList() {\n const conversations = this.getConversationList();\n if (conversations.length) {\n this.emit('conversationsChanged', conversations);\n }\n if (this.currentUser?.userId) {\n this.fetchRemoteConversations().then(remoteConversations => {\n this.emit('conversationsChanged', remoteConversations);\n }).catch(() => {});\n }\n return conversations;\n }\n async fetchRemoteConversations(limit = 50) {\n if (!this.currentUser?.userId) {\n return [];\n }\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.messagesApi, {\n userId: this.currentUser.userId,\n limit\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '会话同步失败');\n }\n return (response.data?.result?.conversations || []).map(conversation => ({\n ...conversation,\n updatedAt: Number(conversation.updatedAt || Date.now()),\n avatarText: conversation.avatarText || String(conversation.title || conversation.target || '?').slice(0, 1).toUpperCase(),\n raw: conversation.raw || {\n type: 'single',\n target: conversation.target,\n line: 0\n }\n }));\n }\n getMessages(conversation) {\n if (!conversation?.id) {\n return [];\n }\n const userId = this.currentUser?.userId || '';\n return applyLocalMessageState(conversation.id, readLocalMessages(userId)[conversation.id] || [], userId);\n }\n async fetchRemoteMessages(conversation, limit = 50, options = {}) {\n if (!conversation?.id || !this.currentUser?.userId) {\n return [];\n }\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.messagesApi, {\n userId: this.currentUser.userId,\n target: conversation.target || conversation.raw?.target || conversation.id.replace(/^single-/, ''),\n limit,\n conversationType: conversation.type || conversation.raw?.type || 0,\n markRead: Boolean(options.markRead),\n includeConversations: Boolean(options.includeConversations)\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '消息同步失败');\n }\n const messages = (response.data?.result?.messages || []).map(message => ({\n ...message,\n displayTimestamp: message.displayTimestamp || message.timestamp,\n conversationId: conversation.id\n }));\n if (options.includeConversations && Array.isArray(response.data?.result?.conversations)) {\n const remoteConversations = response.data.result.conversations.map(remoteConversation => ({\n ...remoteConversation,\n updatedAt: Number(remoteConversation.updatedAt || Date.now()),\n avatarText: remoteConversation.avatarText || String(remoteConversation.title || remoteConversation.target || '?').slice(0, 1).toUpperCase(),\n raw: remoteConversation.raw || {\n type: 'single',\n target: remoteConversation.target,\n line: 0\n }\n }));\n this.emit('conversationsChanged', remoteConversations);\n }\n const userId = this.currentUser?.userId || '';\n const messagesByConversation = readLocalMessages(userId);\n messagesByConversation[conversation.id] = mergeSyncedMessages(messagesByConversation[conversation.id] || [], messages, conversation.id, userId);\n writeLocalMessages(messagesByConversation, userId);\n return messagesByConversation[conversation.id];\n }\n hideMessages(conversation, messages) {\n const items = Array.isArray(messages) ? messages : [messages];\n if (!conversation?.id || !items.length) {\n return;\n }\n const userId = this.currentUser?.userId || '';\n storeMessageKeys(LOCAL_HIDDEN_MESSAGE_STORAGE_KEY, conversation.id, items, userId);\n const messagesByConversation = readLocalMessages(userId);\n messagesByConversation[conversation.id] = applyLocalMessageState(conversation.id, messagesByConversation[conversation.id] || [], userId);\n writeLocalMessages(messagesByConversation, userId);\n }\n markMessagesRecalled(conversation, messages) {\n const items = Array.isArray(messages) ? messages : [messages];\n if (!conversation?.id || !items.length) {\n return;\n }\n const userId = this.currentUser?.userId || '';\n storeMessageKeys(LOCAL_RECALLED_MESSAGE_STORAGE_KEY, conversation.id, items, userId);\n const messagesByConversation = readLocalMessages(userId);\n messagesByConversation[conversation.id] = applyLocalMessageState(conversation.id, messagesByConversation[conversation.id] || [], userId);\n writeLocalMessages(messagesByConversation, userId);\n }\n async recallMessage(conversation, message) {\n const messageUid = String(message?.messageUid || message?.id || '').trim();\n if (!/^\\d+$/.test(messageUid)) {\n throw new Error('缺少可撤回的消息ID');\n }\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.messageActionApi, {\n action: 'recall',\n userId: this.currentUser?.userId || message.from || '',\n messageUid\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '撤回失败');\n }\n this.markMessagesRecalled(conversation, [message]);\n return response.data.result;\n }\n async editMessage(conversation, message, text) {\n const messageUid = String(message?.messageUid || message?.id || '').trim();\n if (!/^\\d+$/.test(messageUid)) {\n throw new Error('缺少可编辑的消息ID');\n }\n const nextText = String(text || '').trim();\n if (!nextText) {\n throw new Error('编辑内容不能为空');\n }\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.messageActionApi, {\n action: 'edit',\n userId: this.currentUser?.userId || message.from || '',\n messageUid,\n text: nextText\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '编辑失败');\n }\n const result = response.data?.result?.message || {};\n const editedMessage = {\n ...message,\n text: result.text || nextText,\n status: 'delivered',\n deliveryStatus: 'delivered',\n editedAt: Number(result.editedAt || Date.now()),\n activityTimestamp: Number(result.editedAt || Date.now())\n };\n saveLocalMessage(editedMessage, this.currentUser?.userId || '');\n this.emit('messageStatusChanged', editedMessage);\n return editedMessage;\n }\n async getMessageDetail(conversation, message) {\n const messageUid = String(message?.messageUid || message?.id || '').trim();\n if (!messageUid) {\n throw new Error('缺少消息ID');\n }\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.messageDetailApi, {\n userId: this.currentUser?.userId || '',\n target: conversation?.target || conversation?.raw?.target || conversation?.id?.replace(/^single-/, '') || '',\n conversationType: conversation?.type || conversation?.raw?.type || 'single',\n messageUid\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '消息详情加载失败');\n }\n return response.data?.result?.detail || null;\n }\n async sendTextMessage(conversation, text, initialMessage = null) {\n const senderRole = conversation?.role || conversation?.raw?.role || '';\n const message = initialMessage || {\n id: randomId(),\n conversationId: conversation.id,\n from: this.currentUser?.userId || 'me',\n senderRole,\n direction: 'out',\n type: 'text',\n text,\n status: 'sending',\n deliveryStatus: 'sending',\n timestamp: Date.now()\n };\n saveLocalMessage(message, this.currentUser?.userId || '');\n if (!initialMessage) {\n this.emit('messageSent', message);\n }\n try {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.sendMessageApi, {\n sender: this.currentUser?.userId || '',\n target: conversation.target || conversation.raw?.target || conversation.id.replace(/^single-/, ''),\n conversationType: conversation.type || conversation.raw?.type || 0,\n text\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '发送失败');\n }\n const sentMessage = {\n ...message,\n status: 'sent',\n deliveryStatus: 'sent',\n messageUid: response.data?.result?.messageUid ? String(response.data.result.messageUid) : '',\n senderRole: response.data?.result?.senderRole || message.senderRole || '',\n serverTimestamp: Number(response.data?.result?.timestamp || 0) || null,\n timestamp: message.timestamp,\n displayTimestamp: message.displayTimestamp || message.timestamp\n };\n saveLocalMessage(sentMessage, this.currentUser?.userId || '');\n this.emit('messageStatusChanged', sentMessage);\n return sentMessage;\n } catch (error) {\n const failedMessage = {\n ...message,\n status: 'failed',\n deliveryStatus: '',\n error: error.response?.data?.message || error.message || '发送失败'\n };\n saveLocalMessage(failedMessage, this.currentUser?.userId || '');\n this.emit('messageStatusChanged', failedMessage);\n return failedMessage;\n }\n }\n async sendAttachmentMessage(conversation, {\n type,\n file,\n text = ''\n }, initialMessage = null) {\n const senderRole = conversation?.role || conversation?.raw?.role || '';\n const message = initialMessage || {\n id: randomId(),\n conversationId: conversation.id,\n from: this.currentUser?.userId || 'me',\n senderRole,\n direction: 'out',\n type,\n text,\n attachment: {\n type,\n name: file?.name || (type === 'image' ? '图片' : '文件'),\n mime: file?.type || '',\n size: file?.size || 0,\n url: type === 'image' && file ? URL.createObjectURL(file) : ''\n },\n status: 'sending',\n deliveryStatus: 'sending',\n timestamp: Date.now()\n };\n saveLocalMessage(message, this.currentUser?.userId || '');\n if (!initialMessage) {\n this.emit('messageSent', message);\n }\n try {\n const formData = new FormData();\n formData.append('sender', this.currentUser?.userId || '');\n formData.append('target', conversation.target || conversation.raw?.target || conversation.id.replace(/^single-/, ''));\n formData.append('conversationType', conversation.type || conversation.raw?.type || 0);\n formData.append('messageType', type);\n formData.append('text', text || '');\n formData.append('attachment', file);\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.sendMessageApi, formData, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '发送失败');\n }\n const sentMessage = {\n ...message,\n status: 'sent',\n deliveryStatus: 'sent',\n messageUid: response.data?.result?.messageUid ? String(response.data.result.messageUid) : '',\n senderRole: response.data?.result?.senderRole || message.senderRole || '',\n attachment: response.data?.result?.attachment || message.attachment,\n serverTimestamp: Number(response.data?.result?.timestamp || 0) || null,\n timestamp: message.timestamp,\n displayTimestamp: message.displayTimestamp || message.timestamp\n };\n saveLocalMessage(sentMessage, this.currentUser?.userId || '');\n this.emit('messageStatusChanged', sentMessage);\n return sentMessage;\n } catch (error) {\n const failedMessage = {\n ...message,\n status: 'failed',\n deliveryStatus: '',\n error: error.response?.data?.message || error.message || '发送失败'\n };\n saveLocalMessage(failedMessage, this.currentUser?.userId || '');\n this.emit('messageStatusChanged', failedMessage);\n return failedMessage;\n }\n }\n getContactList() {\n return [];\n }\n async getServerContactList(userId) {\n if (!userId) {\n return [];\n }\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.contactsApi, {\n userId\n }, {\n withCredentials: false,\n headers: this.getAppAuthHeaders(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.contactsApi)\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '联系人加载失败');\n }\n return (response.data?.result?.contacts || []).map(item => {\n return this.mapContact(item.uid, {\n alias: item.alias\n }, {\n uid: item.uid,\n displayName: item.displayName,\n name: item.name,\n mobile: item.mobile,\n portrait: item.portrait,\n address: item.address,\n friendAlias: item.alias\n });\n }).sort((a, b) => a.title.localeCompare(b.title, 'zh-CN'));\n }\n async addContact(userId, keyword, reason = '') {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.addContactApi, {\n userId,\n keyword,\n reason\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '添加联系人失败');\n }\n const item = response.data?.result?.contact;\n if (!item?.uid) {\n return null;\n }\n return this.mapContact(item.uid, {\n alias: item.alias\n }, {\n uid: item.uid,\n displayName: item.displayName,\n name: item.name,\n mobile: item.mobile,\n portrait: item.portrait,\n address: item.address,\n friendAlias: item.alias\n });\n }\n normalizeConversation(conversation) {\n const title = getTitle(conversation?.title, conversation?.target || '');\n return {\n ...conversation,\n id: conversation?.id || `${conversation?.type === 'single' ? 'single' : 'group'}-${conversation?.target}`,\n target: conversation?.target || conversation?.raw?.target || '',\n title,\n subtitle: conversation?.subtitle || '',\n unread: Number(conversation?.unread || 0),\n portrait: conversation?.portrait || '',\n avatarText: conversation?.avatarText || title.slice(0, 1).toUpperCase(),\n updatedAt: Number(conversation?.updatedAt || Date.now()),\n raw: conversation?.raw || {\n type: conversation?.type === 'single' ? 'single' : 'group',\n target: conversation?.target || '',\n line: 0\n }\n };\n }\n async createGroup(userId, {\n name,\n memberIds = [],\n announcement = '',\n welcomeText = '',\n channel = false\n }) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.conversationActionsApi, {\n action: channel ? 'create_channel' : 'create_group',\n userId,\n name,\n memberIds,\n announcement,\n welcomeText\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || (channel ? '创建公告群失败' : '创建群组失败'));\n }\n return this.normalizeConversation(response.data?.result?.conversation || {});\n }\n async joinGroup(userId, groupKey) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.conversationActionsApi, {\n action: 'join_group',\n userId,\n groupKey\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '加入群组失败');\n }\n return this.normalizeConversation(response.data?.result?.conversation || {});\n }\n async broadcastMessage(userId, targetIds, text) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.conversationActionsApi, {\n action: 'broadcast',\n userId,\n targetIds,\n text\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '群发失败');\n }\n return response.data?.result || {};\n }\n async getGroupDetail(userId, groupId) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.conversationActionsApi, {\n action: 'group_detail',\n userId,\n groupId\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '群资料加载失败');\n }\n return response.data?.result?.group || null;\n }\n async getGroupMembers(userId, groupId) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.conversationActionsApi, {\n action: 'group_members',\n userId,\n groupId\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '群员加载失败');\n }\n return response.data?.result?.members || [];\n }\n async updateGroupAlias(userId, groupId, alias) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.conversationActionsApi, {\n action: 'update_group_alias',\n userId,\n groupId,\n alias\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '本群昵称保存失败');\n }\n return response.data?.result?.alias || '';\n }\n async leaveGroup(userId, groupId) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.conversationActionsApi, {\n action: 'leave_group',\n userId,\n groupId\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '退出群组失败');\n }\n return response.data?.result || {};\n }\n async dismissGroup(userId, groupId) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.conversationActionsApi, {\n action: 'dismiss_group',\n userId,\n groupId\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '解散群组失败');\n }\n return response.data?.result || {};\n }\n async transferGroup(userId, groupId, targetUserId) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.conversationActionsApi, {\n action: 'transfer_group',\n userId,\n groupId,\n targetUserId\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '转让群组失败');\n }\n return response.data?.result || {};\n }\n async setGroupAdmins(userId, groupId, adminIds) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.conversationActionsApi, {\n action: 'set_group_admins',\n userId,\n groupId,\n adminIds\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '管理员设置失败');\n }\n return response.data?.result || {};\n }\n async updateGroupProfile(userId, groupId, {\n name,\n announcement,\n welcomeText\n }) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.conversationActionsApi, {\n action: 'update_group_profile',\n userId,\n groupId,\n name,\n announcement,\n welcomeText\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '群资料保存失败');\n }\n return response.data?.result || {};\n }\n async kickGroupMembers(userId, groupId, targetIds) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.conversationActionsApi, {\n action: 'kick_group_members',\n userId,\n groupId,\n targetIds\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '踢人失败');\n }\n return response.data?.result || {};\n }\n async updateGroupMuteSetting(userId, groupId, setting) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.conversationActionsApi, {\n action: 'update_mute_setting',\n userId,\n groupId,\n ...setting\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '禁言设置保存失败');\n }\n return response.data?.result?.muteSetting || null;\n }\n async updateContactAlias(userId, targetId, alias) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.updateAliasApi, {\n userId,\n targetId,\n alias\n }, {\n withCredentials: false,\n headers: this.getAppAuthHeaders(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.updateAliasApi)\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '备注名修改失败');\n }\n return response.data?.result?.contact?.alias || '';\n }\n async getProfile(userId) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.profileApi, {\n action: 'get',\n userId\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '用户资料加载失败');\n }\n return response.data?.result?.profile || null;\n }\n async updateProfile(userId, {\n displayName,\n portraitFile\n }) {\n const formData = new FormData();\n formData.append('action', 'update');\n formData.append('userId', userId);\n formData.append('displayName', displayName);\n if (portraitFile) {\n formData.append('portraitFile', portraitFile);\n }\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.profileApi, formData, {\n withCredentials: false,\n headers: this.getAppAuthHeaders(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.appServer)\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || '用户资料保存失败');\n }\n return response.data?.result?.profile || null;\n }\n async changePassword({\n oldPassword,\n newPassword\n }) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.changePasswordApi, {\n oldPassword,\n newPassword\n }, {\n withCredentials: false,\n headers: this.getAppAuthHeaders(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.changePasswordApi)\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || response.data?.msg || '密码修改失败');\n }\n return true;\n }\n async resetPasswordWithCode({\n mobile,\n resetCode,\n newPassword\n }) {\n const response = await axios__WEBPACK_IMPORTED_MODULE_17__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.resetPasswordApi, {\n mobile,\n resetCode,\n newPassword\n }, {\n withCredentials: false\n });\n if (response.data?.code !== 0) {\n throw new Error(response.data?.message || response.data?.msg || '密码设置失败');\n }\n return true;\n }\n refreshContactList() {\n const contacts = this.getContactList();\n this.emit('contactsChanged', contacts);\n return contacts;\n }\n createSingleConversation(userId, userInfo = null) {\n const title = getTitle(userInfo?.title || userInfo?.displayName || userInfo?.name, userId);\n return {\n id: `single-${userId}`,\n type: 'single',\n target: userId,\n title,\n subtitle: '',\n unread: 0,\n portrait: userInfo?.portrait || '',\n avatarText: title.slice(0, 1).toUpperCase(),\n updatedAt: Date.now(),\n raw: {\n type: 'single',\n target: userId,\n line: 0\n }\n };\n }\n mapContact(userId, friend, userInfo) {\n const title = getTitle(userInfo?.friendAlias || friend?.alias || userInfo?.displayName || userInfo?.name, userId);\n return {\n id: userId,\n uid: userId,\n title,\n subtitle: userInfo?.mobile || userInfo?.name || userId,\n portrait: userInfo?.portrait || '',\n address: userInfo?.address || '',\n avatarText: title.slice(0, 1).toUpperCase(),\n alias: friend?.alias || userInfo?.friendAlias || '',\n rawFriend: friend,\n rawUser: userInfo\n };\n }\n on(eventName, handler) {\n if (!this.listeners.has(eventName)) {\n this.listeners.set(eventName, new Set());\n }\n this.listeners.get(eventName).add(handler);\n return () => this.off(eventName, handler);\n }\n off(eventName, handler) {\n this.listeners.get(eventName)?.delete(handler);\n }\n emit(eventName, payload) {\n this.listeners.get(eventName)?.forEach(handler => handler(payload));\n }\n persistAppToken(url, response) {\n const appToken = response.headers?.authtoken || response.headers?.authToken;\n if (appToken) {\n localStorage.setItem(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.authTokenStoragePrefix + this.getUrlHost(url), appToken);\n }\n }\n getAppAuthHeaders(url) {\n const host = this.getUrlHost(url);\n if (!host) {\n return {};\n }\n const token = localStorage.getItem(_config__WEBPACK_IMPORTED_MODULE_18__.appConfig.authTokenStoragePrefix + host);\n return token ? {\n authToken: token\n } : {};\n }\n getUrlHost(url) {\n try {\n return new URL(url, window.location.origin).host;\n } catch (error) {\n return '';\n }\n }\n}\nconst imClient = new HttpImClient();\n\n//# sourceURL=webpack://jc-web-im/./src/services/httpImClient.js?\n}"); + +/***/ }), + +/***/ "./src/services/imClient.js": +/*!**********************************!*\ + !*** ./src/services/imClient.js ***! + \**********************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ DocumentedHttpImClient: function() { return /* reexport safe */ _documentedHttpImClient__WEBPACK_IMPORTED_MODULE_3__.DocumentedHttpImClient; },\n/* harmony export */ HttpImClient: function() { return /* reexport safe */ _httpImClient__WEBPACK_IMPORTED_MODULE_1__.HttpImClient; },\n/* harmony export */ LongConnectionImClient: function() { return /* reexport safe */ _longConnectionImClient__WEBPACK_IMPORTED_MODULE_2__.LongConnectionImClient; },\n/* harmony export */ WildfireImClient: function() { return /* binding */ UnsupportedWildfireImClient; },\n/* harmony export */ imClient: function() { return /* binding */ imClient; }\n/* harmony export */ });\n/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @/config */ \"./src/config.js\");\n/* harmony import */ var _httpImClient__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./httpImClient */ \"./src/services/httpImClient.js\");\n/* harmony import */ var _longConnectionImClient__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./longConnectionImClient */ \"./src/services/longConnectionImClient.js\");\n/* harmony import */ var _documentedHttpImClient__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./documentedHttpImClient */ \"./src/services/documentedHttpImClient.js\");\n\n\n\n\nclass UnsupportedWildfireImClient {\n constructor() {\n throw new Error('当前本地社区版 Web 客户端未启用 WildFire Web SDK');\n }\n}\nconst imClient = _config__WEBPACK_IMPORTED_MODULE_0__.appConfig.sdkMode === 'wildfire' ? new UnsupportedWildfireImClient() : _config__WEBPACK_IMPORTED_MODULE_0__.appConfig.sdkMode === 'custom-ws' || _config__WEBPACK_IMPORTED_MODULE_0__.appConfig.sdkMode === 'long' ? new _longConnectionImClient__WEBPACK_IMPORTED_MODULE_2__.LongConnectionImClient() : _config__WEBPACK_IMPORTED_MODULE_0__.appConfig.sdkMode === 'legacy-http' ? new _httpImClient__WEBPACK_IMPORTED_MODULE_1__.HttpImClient() : new _documentedHttpImClient__WEBPACK_IMPORTED_MODULE_3__.DocumentedHttpImClient();\n\n\n//# sourceURL=webpack://jc-web-im/./src/services/imClient.js?\n}"); + +/***/ }), + +/***/ "./src/services/longConnectionImClient.js": +/*!************************************************!*\ + !*** ./src/services/longConnectionImClient.js ***! + \************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ LongConnectionImClient: function() { return /* binding */ LongConnectionImClient; }\n/* harmony export */ });\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.iterator.constructor.js */ \"./node_modules/core-js/modules/es.iterator.constructor.js\");\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.iterator.filter.js */ \"./node_modules/core-js/modules/es.iterator.filter.js\");\n/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/es.iterator.for-each.js */ \"./node_modules/core-js/modules/es.iterator.for-each.js\");\n/* harmony import */ var core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! core-js/modules/es.iterator.map.js */ \"./node_modules/core-js/modules/es.iterator.map.js\");\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @/config */ \"./src/config.js\");\n/* harmony import */ var _httpImClient__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./httpImClient */ \"./src/services/httpImClient.js\");\n\n\n\n\n\n\nconst HEARTBEAT_INTERVAL_MS = 25000;\nconst RECONNECT_MIN_MS = 1000;\nconst RECONNECT_MAX_MS = 15000;\nfunction buildWsUrl() {\n if (_config__WEBPACK_IMPORTED_MODULE_4__.appConfig.wsUrl) {\n return _config__WEBPACK_IMPORTED_MODULE_4__.appConfig.wsUrl;\n }\n const protocol = _config__WEBPACK_IMPORTED_MODULE_4__.appConfig.useWss ? 'wss' : 'ws';\n const host = _config__WEBPACK_IMPORTED_MODULE_4__.appConfig.imServerHost || window.location.hostname;\n const port = _config__WEBPACK_IMPORTED_MODULE_4__.appConfig.imRoutePort ? `:${_config__WEBPACK_IMPORTED_MODULE_4__.appConfig.imRoutePort}` : '';\n const path = _config__WEBPACK_IMPORTED_MODULE_4__.appConfig.wsPath?.startsWith('/') ? _config__WEBPACK_IMPORTED_MODULE_4__.appConfig.wsPath : `/${_config__WEBPACK_IMPORTED_MODULE_4__.appConfig.wsPath || 'ws'}`;\n return `${protocol}://${host}${port}${path}`;\n}\nfunction normalizeConversationId(payload) {\n const target = payload.target || payload.to || payload.receiver || payload.conversationTarget || payload.from || payload.sender;\n return payload.conversationId || (target ? `single-${target}` : '');\n}\nfunction normalizeIncomingMessage(payload, currentUserId) {\n const item = payload.message || payload;\n const from = item.from || item.sender || item.fromUser || '';\n const target = item.target || item.to || item.receiver || '';\n const direction = from && String(from) === String(currentUserId) ? 'out' : 'in';\n const conversationId = item.conversationId || normalizeConversationId({\n ...item,\n target: direction === 'out' ? target : from\n });\n if (!conversationId) {\n return null;\n }\n const status = item.status || item.deliveryStatus || 'sent';\n return {\n id: item.id || item.messageId || item.messageUid || `${Date.now()}${Math.random().toString(16).slice(2)}`,\n messageUid: item.messageUid || item.uid || item.messageId || '',\n conversationId,\n from,\n target,\n direction,\n type: item.type || 'text',\n text: item.text || item.content || item.searchableContent || '',\n status,\n deliveryStatus: item.deliveryStatus || (direction === 'out' ? status : ''),\n timestamp: Number(item.timestamp || item.serverTimestamp || Date.now()),\n displayTimestamp: Number(item.displayTimestamp || item.timestamp || item.serverTimestamp || Date.now()),\n raw: item\n };\n}\nclass LongConnectionImClient extends _httpImClient__WEBPACK_IMPORTED_MODULE_5__.HttpImClient {\n constructor() {\n super();\n this.socket = null;\n this.wsUrl = buildWsUrl();\n this.heartbeatTimer = null;\n this.reconnectTimer = null;\n this.reconnectAttempts = 0;\n this.manualClose = false;\n }\n getSdkVersion() {\n return 'JC-LC/0.1';\n }\n shouldUseMessagePolling() {\n return !this.connected;\n }\n async ensureSdk() {\n if (typeof WebSocket === 'undefined') {\n throw new Error('当前浏览器不支持 WebSocket 长连接');\n }\n return true;\n }\n connect(userId, token) {\n this.currentUser = {\n userId,\n token\n };\n this.manualClose = false;\n this.openSocket();\n }\n disconnect() {\n this.manualClose = true;\n this.clearReconnectTimer();\n this.clearHeartbeatTimer();\n if (this.socket) {\n this.socket.close();\n this.socket = null;\n }\n super.disconnect();\n }\n openSocket() {\n if (this.socket?.readyState === WebSocket.OPEN) {\n this.sendAuthFrame();\n return;\n }\n if (this.socket?.readyState === WebSocket.CONNECTING) {\n return;\n }\n this.emit('connectionStatusChanged', {\n connected: false,\n statusText: '长连接连接中'\n });\n try {\n this.socket = new WebSocket(this.wsUrl);\n this.socket.binaryType = 'arraybuffer';\n } catch (error) {\n this.emit('connectionStatusChanged', {\n connected: false,\n statusText: '长连接创建失败,使用HTTP补偿'\n });\n this.scheduleReconnect();\n return;\n }\n this.socket.onopen = () => {\n this.connected = true;\n this.reconnectAttempts = 0;\n this.emit('connectionStatusChanged', {\n connected: true,\n statusText: '长连接已连接'\n });\n this.sendAuthFrame();\n this.startHeartbeat();\n this.refreshConversationList();\n };\n this.socket.onmessage = event => {\n this.handleFrame(event.data);\n };\n this.socket.onerror = () => {\n this.emit('connectionStatusChanged', {\n connected: false,\n statusText: '长连接异常,使用HTTP补偿'\n });\n };\n this.socket.onclose = () => {\n this.connected = false;\n this.clearHeartbeatTimer();\n this.socket = null;\n if (this.manualClose) {\n return;\n }\n this.emit('connectionStatusChanged', {\n connected: false,\n statusText: '长连接重连中'\n });\n this.scheduleReconnect();\n };\n }\n sendAuthFrame() {\n this.sendFrame({\n type: 'auth',\n userId: this.currentUser?.userId || '',\n token: this.currentUser?.token || '',\n clientId: this.getClientId(),\n platform: _config__WEBPACK_IMPORTED_MODULE_4__.appConfig.platform,\n timestamp: Date.now()\n });\n }\n sendFrame(frame) {\n if (this.socket?.readyState !== WebSocket.OPEN) {\n return false;\n }\n this.socket.send(JSON.stringify(frame));\n return true;\n }\n handleFrame(rawData) {\n if (rawData instanceof ArrayBuffer) {\n this.emit('rawFrameReceived', rawData);\n return;\n }\n let payload;\n try {\n payload = typeof rawData === 'string' ? JSON.parse(rawData) : rawData;\n } catch (error) {\n return;\n }\n const type = payload.type || payload.event || payload.cmd || '';\n if (type === 'pong' || type === 'auth_ok' || type === 'connected') {\n return;\n }\n if (type === 'contactsChanged') {\n this.emit('contactsChanged', payload.contacts || []);\n return;\n }\n if (type === 'conversationsChanged') {\n this.emit('conversationsChanged', payload.conversations || []);\n return;\n }\n if (type === 'messages' && Array.isArray(payload.messages)) {\n payload.messages.map(message => normalizeIncomingMessage(message, this.currentUser?.userId)).filter(Boolean).forEach(message => this.emit('messageReceived', message));\n return;\n }\n const message = normalizeIncomingMessage(payload, this.currentUser?.userId);\n if (message) {\n this.emit(message.direction === 'out' ? 'messageStatusChanged' : 'messageReceived', message);\n }\n }\n startHeartbeat() {\n this.clearHeartbeatTimer();\n this.heartbeatTimer = window.setInterval(() => {\n this.sendFrame({\n type: 'ping',\n timestamp: Date.now()\n });\n }, HEARTBEAT_INTERVAL_MS);\n }\n clearHeartbeatTimer() {\n if (this.heartbeatTimer) {\n window.clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n scheduleReconnect() {\n this.clearReconnectTimer();\n const delay = Math.min(RECONNECT_MAX_MS, RECONNECT_MIN_MS * 2 ** this.reconnectAttempts);\n this.reconnectAttempts += 1;\n this.reconnectTimer = window.setTimeout(() => {\n this.openSocket();\n }, delay);\n }\n clearReconnectTimer() {\n if (this.reconnectTimer) {\n window.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n }\n }\n}\n\n//# sourceURL=webpack://jc-web-im/./src/services/longConnectionImClient.js?\n}"); + +/***/ }), + +/***/ "./src/services/mediaProxy.js": +/*!************************************!*\ + !*** ./src/services/mediaProxy.js ***! + \************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ imageDisplayUrl: function() { return /* binding */ imageDisplayUrl; }\n/* harmony export */ });\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.iterator.constructor.js */ \"./node_modules/core-js/modules/es.iterator.constructor.js\");\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.iterator.filter.js */ \"./node_modules/core-js/modules/es.iterator.filter.js\");\n/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/es.iterator.map.js */ \"./node_modules/core-js/modules/es.iterator.map.js\");\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! core-js/modules/es.set.difference.v2.js */ \"./node_modules/core-js/modules/es.set.difference.v2.js\");\n/* harmony import */ var core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! core-js/modules/es.set.intersection.v2.js */ \"./node_modules/core-js/modules/es.set.intersection.v2.js\");\n/* harmony import */ var core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_4__);\n/* harmony import */ var core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! core-js/modules/es.set.is-disjoint-from.v2.js */ \"./node_modules/core-js/modules/es.set.is-disjoint-from.v2.js\");\n/* harmony import */ var core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! core-js/modules/es.set.is-subset-of.v2.js */ \"./node_modules/core-js/modules/es.set.is-subset-of.v2.js\");\n/* harmony import */ var core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_6__);\n/* harmony import */ var core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! core-js/modules/es.set.is-superset-of.v2.js */ \"./node_modules/core-js/modules/es.set.is-superset-of.v2.js\");\n/* harmony import */ var core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_7__);\n/* harmony import */ var core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! core-js/modules/es.set.symmetric-difference.v2.js */ \"./node_modules/core-js/modules/es.set.symmetric-difference.v2.js\");\n/* harmony import */ var core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_8__);\n/* harmony import */ var core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! core-js/modules/es.set.union.v2.js */ \"./node_modules/core-js/modules/es.set.union.v2.js\");\n/* harmony import */ var core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_9___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_9__);\n\n\n\n\n\n\n\n\n\n\nfunction configuredProxyHosts() {\n const configured = window.JC_WEB_IM_CONFIG?.mediaProxyHosts;\n const values = Array.isArray(configured) ? configured : String(configured || '').split(/[\\s,]+/);\n return new Set(values.map(value => String(value || '').trim().toLowerCase().replace(/\\.$/, '')).filter(Boolean));\n}\nfunction imageDisplayUrl(value) {\n const source = String(value || '').trim();\n if (!source || source.startsWith('blob:') || source.startsWith('data:')) {\n return source;\n }\n try {\n const url = new URL(source, window.location.origin);\n if (url.protocol !== 'https:' || !configuredProxyHosts().has(url.hostname.toLowerCase())) {\n return source;\n }\n const proxyUrl = new URL('/api/media_proxy.php', window.location.origin);\n proxyUrl.searchParams.set('url', url.toString());\n return proxyUrl.toString();\n } catch (error) {\n return source;\n }\n}\n\n//# sourceURL=webpack://jc-web-im/./src/services/mediaProxy.js?\n}"); + +/***/ }), + +/***/ "./src/services/realtimeNotifications.js": +/*!***********************************************!*\ + !*** ./src/services/realtimeNotifications.js ***! + \***********************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ realtimeNotifications: function() { return /* binding */ realtimeNotifications; }\n/* harmony export */ });\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.includes.js */ \"./node_modules/core-js/modules/es.array.includes.js\");\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var axios__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! axios */ \"./node_modules/axios/lib/axios.js\");\n/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @/config */ \"./src/config.js\");\n/* harmony import */ var _services_imClient__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @/services/imClient */ \"./src/services/imClient.js\");\n\n\n\n\nclass RealtimeNotificationClient {\n constructor() {\n this.source = null;\n this.userId = '';\n this.handler = null;\n this.reconnectTimer = null;\n this.reconnectAttempt = 0;\n this.generation = 0;\n }\n isConnected() {\n return this.source?.readyState === WebSocket.OPEN;\n }\n start(userId, handler) {\n this.stop();\n if (!_config__WEBPACK_IMPORTED_MODULE_2__.appConfig.sseEnabled || !userId || typeof window.WebSocket !== 'function') return;\n this.userId = userId;\n this.handler = handler;\n this.generation += 1;\n this.open(this.generation);\n }\n stop() {\n this.generation += 1;\n window.clearTimeout(this.reconnectTimer);\n this.reconnectTimer = null;\n if (this.source) this.source.close();\n this.source = null;\n this.userId = '';\n this.handler = null;\n this.reconnectAttempt = 0;\n }\n async open(generation) {\n try {\n const response = await axios__WEBPACK_IMPORTED_MODULE_1__[\"default\"].post(_config__WEBPACK_IMPORTED_MODULE_2__.appConfig.sseTicketApi, {\n userId: this.userId\n }, {\n withCredentials: false,\n headers: _services_imClient__WEBPACK_IMPORTED_MODULE_3__.imClient.getAppAuthHeaders(_config__WEBPACK_IMPORTED_MODULE_2__.appConfig.appServer)\n });\n if (generation !== this.generation) return;\n if (response.data?.code !== 0) throw new Error(response.data?.message || '实时通知凭证申请失败');\n const ticket = response.data?.result?.ticket || '';\n if (!ticket) throw new Error('实时通知凭证为空');\n const separator = _config__WEBPACK_IMPORTED_MODULE_2__.appConfig.sseUrl.includes('?') ? '&' : '?';\n const endpoint = `${_config__WEBPACK_IMPORTED_MODULE_2__.appConfig.sseUrl}${separator}ticket=${encodeURIComponent(ticket)}`;\n const socketUrl = endpoint.startsWith('ws://') || endpoint.startsWith('wss://') ? endpoint : `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}${endpoint.startsWith('/') ? '' : '/'}${endpoint}`;\n const source = new WebSocket(socketUrl);\n this.source = source;\n source.onopen = () => {\n this.reconnectAttempt = 0;\n };\n source.onmessage = event => {\n if (generation !== this.generation) return;\n let payload = {};\n try {\n payload = JSON.parse(event.data || '{}');\n } catch (error) {\n payload = {};\n }\n if (payload.type !== 'message-notification') return;\n this.handler?.(payload);\n };\n source.onerror = () => {\n if (generation !== this.generation) return;\n source.close();\n if (this.source === source) this.source = null;\n this.scheduleReconnect(generation);\n };\n source.onclose = () => {\n if (generation !== this.generation || this.source !== source) return;\n this.source = null;\n this.scheduleReconnect(generation);\n };\n } catch (error) {\n if (generation === this.generation) this.scheduleReconnect(generation);\n }\n }\n scheduleReconnect(generation) {\n window.clearTimeout(this.reconnectTimer);\n const delay = Math.min(30000, 1000 * 2 ** Math.min(this.reconnectAttempt, 5));\n this.reconnectAttempt += 1;\n this.reconnectTimer = window.setTimeout(() => this.open(generation), delay);\n }\n}\nconst realtimeNotifications = new RealtimeNotificationClient();\n\n//# sourceURL=webpack://jc-web-im/./src/services/realtimeNotifications.js?\n}"); + +/***/ }), + +/***/ "./src/stores/contacts.js": +/*!********************************!*\ + !*** ./src/stores/contacts.js ***! + \********************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ useContactStore: function() { return /* binding */ useContactStore; }\n/* harmony export */ });\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.iterator.constructor.js */ \"./node_modules/core-js/modules/es.iterator.constructor.js\");\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.iterator.find.js */ \"./node_modules/core-js/modules/es.iterator.find.js\");\n/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/es.iterator.for-each.js */ \"./node_modules/core-js/modules/es.iterator.for-each.js\");\n/* harmony import */ var core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! core-js/modules/es.iterator.map.js */ \"./node_modules/core-js/modules/es.iterator.map.js\");\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var core_js_modules_es_map_get_or_insert_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! core-js/modules/es.map.get-or-insert.js */ \"./node_modules/core-js/modules/es.map.get-or-insert.js\");\n/* harmony import */ var core_js_modules_es_map_get_or_insert_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_map_get_or_insert_js__WEBPACK_IMPORTED_MODULE_4__);\n/* harmony import */ var core_js_modules_es_map_get_or_insert_computed_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! core-js/modules/es.map.get-or-insert-computed.js */ \"./node_modules/core-js/modules/es.map.get-or-insert-computed.js\");\n/* harmony import */ var core_js_modules_es_map_get_or_insert_computed_js__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_map_get_or_insert_computed_js__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var pinia__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! pinia */ \"./node_modules/pinia/dist/pinia.mjs\");\n/* harmony import */ var _services_imClient__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! @/services/imClient */ \"./src/services/imClient.js\");\n/* harmony import */ var _stores_conversations__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! @/stores/conversations */ \"./src/stores/conversations.js\");\n/* harmony import */ var _stores_navigation__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! @/stores/navigation */ \"./src/stores/navigation.js\");\n/* harmony import */ var _stores_session__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! @/stores/session */ \"./src/stores/session.js\");\n\n\n\n\n\n\n\n\n\n\n\nconst CONTACT_CACHE_MS = 15000;\nfunction normalizeContacts(contacts) {\n const unique = new Map();\n contacts.forEach(contact => {\n const uid = String(contact.uid || contact.id || '').trim();\n if (!uid) {\n return;\n }\n const normalized = {\n ...contact,\n id: String(contact.id || uid),\n uid\n };\n unique.set(uid, {\n ...(unique.get(uid) || {}),\n ...normalized\n });\n });\n return Array.from(unique.values()).sort((a, b) => a.title.localeCompare(b.title, 'zh-CN'));\n}\nconst useContactStore = (0,pinia__WEBPACK_IMPORTED_MODULE_6__.defineStore)('contacts', {\n state: () => ({\n contacts: [],\n activeId: '',\n loading: false,\n eventsBound: false,\n refreshSerial: 0,\n refreshPromise: null,\n loadedForUserId: '',\n loadedAt: 0,\n actionError: '',\n actionInfo: ''\n }),\n getters: {\n activeContact(state) {\n return state.contacts.find(item => item.id === state.activeId) || state.contacts[0] || null;\n }\n },\n actions: {\n bindImEvents() {\n if (this.eventsBound) {\n return;\n }\n this.eventsBound = true;\n _services_imClient__WEBPACK_IMPORTED_MODULE_7__.imClient.on('contactsChanged', contacts => {\n if (!contacts.length && this.contacts.length) {\n return;\n }\n this.contacts = normalizeContacts(contacts);\n if (!this.contacts.find(item => item.id === this.activeId)) {\n this.activeId = this.contacts[0]?.id || '';\n }\n });\n },\n async refreshContacts(options = {}) {\n this.bindImEvents();\n const session = (0,_stores_session__WEBPACK_IMPORTED_MODULE_10__.useSessionStore)();\n const requestUserId = session.userId;\n const force = Boolean(options.force);\n if (!requestUserId) {\n this.clearContacts();\n return [];\n }\n if (!force && this.contacts.length && this.loadedForUserId === requestUserId && Date.now() - this.loadedAt < CONTACT_CACHE_MS) {\n return this.contacts;\n }\n if (this.refreshPromise && !force) {\n return this.refreshPromise;\n }\n const serial = this.refreshSerial + 1;\n this.refreshSerial = serial;\n this.loading = true;\n this.refreshPromise = (async () => {\n this.contacts = normalizeContacts(_services_imClient__WEBPACK_IMPORTED_MODULE_7__.imClient.refreshContactList());\n if (!this.contacts.find(item => item.id === this.activeId)) {\n this.activeId = this.contacts[0]?.id || '';\n }\n if (!this.contacts.length || force) {\n const serverContacts = await _services_imClient__WEBPACK_IMPORTED_MODULE_7__.imClient.getServerContactList(requestUserId);\n if (serial !== this.refreshSerial || (0,_stores_session__WEBPACK_IMPORTED_MODULE_10__.useSessionStore)().userId !== requestUserId) {\n return this.contacts;\n }\n this.contacts = normalizeContacts(serverContacts);\n this.loadedForUserId = requestUserId;\n this.loadedAt = Date.now();\n if (!this.contacts.find(item => item.id === this.activeId)) {\n this.activeId = this.contacts[0]?.id || '';\n }\n }\n window.setTimeout(() => {\n if (serial !== this.refreshSerial || (0,_stores_session__WEBPACK_IMPORTED_MODULE_10__.useSessionStore)().userId !== requestUserId) {\n return;\n }\n const latest = _services_imClient__WEBPACK_IMPORTED_MODULE_7__.imClient.getContactList(false);\n if (latest.length || !this.contacts.length) {\n this.contacts = normalizeContacts(latest);\n this.loadedForUserId = requestUserId;\n this.loadedAt = Date.now();\n if (!this.contacts.find(item => item.id === this.activeId)) {\n this.activeId = this.contacts[0]?.id || '';\n }\n }\n }, 1200);\n return this.contacts;\n })();\n try {\n return await this.refreshPromise;\n } finally {\n if (serial === this.refreshSerial) {\n this.loading = false;\n this.refreshPromise = null;\n }\n }\n },\n async addContact(keyword) {\n const session = (0,_stores_session__WEBPACK_IMPORTED_MODULE_10__.useSessionStore)();\n const value = String(keyword || '').trim();\n this.actionError = '';\n this.actionInfo = '';\n if (!session.userId) {\n this.actionError = '请先登录';\n return false;\n }\n if (!value) {\n this.actionError = '请输入用户ID、手机号或用户名';\n return false;\n }\n this.loading = true;\n try {\n const contact = await _services_imClient__WEBPACK_IMPORTED_MODULE_7__.imClient.addContact(session.userId, value, '添加联系人');\n if (contact) {\n this.contacts = normalizeContacts([...this.contacts, contact]);\n this.activeId = contact.id;\n }\n this.actionInfo = '联系人已添加';\n await this.refreshContacts({\n force: true\n });\n return true;\n } catch (error) {\n this.actionError = error.message || '添加联系人失败';\n return false;\n } finally {\n this.loading = false;\n }\n },\n async updateContactAlias(contactId, alias) {\n const session = (0,_stores_session__WEBPACK_IMPORTED_MODULE_10__.useSessionStore)();\n const targetId = String(contactId || '').trim();\n const nextAlias = String(alias || '').trim();\n this.actionError = '';\n this.actionInfo = '';\n if (!session.userId) {\n this.actionError = '请先登录';\n return false;\n }\n if (!targetId) {\n this.actionError = '缺少联系人ID';\n return false;\n }\n try {\n const savedAlias = await _services_imClient__WEBPACK_IMPORTED_MODULE_7__.imClient.updateContactAlias(session.userId, targetId, nextAlias);\n this.contacts = normalizeContacts(this.contacts.map(item => {\n if (item.uid !== targetId && item.id !== targetId) {\n return item;\n }\n const rawUser = {\n ...(item.rawUser || {}),\n friendAlias: savedAlias\n };\n const rawFriend = {\n ...(item.rawFriend || {}),\n alias: savedAlias\n };\n const title = savedAlias || rawUser.displayName || rawUser.name || item.uid;\n return {\n ...item,\n title,\n avatarText: title.slice(0, 1).toUpperCase(),\n alias: savedAlias,\n rawFriend,\n rawUser\n };\n }));\n this.loadedAt = Date.now();\n this.actionInfo = '备注名已修改';\n return true;\n } catch (error) {\n this.actionError = error.message || '备注名修改失败';\n return false;\n }\n },\n clearContacts() {\n this.refreshSerial += 1;\n this.contacts = [];\n this.activeId = '';\n this.loading = false;\n this.refreshPromise = null;\n this.loadedForUserId = '';\n this.loadedAt = 0;\n this.actionError = '';\n this.actionInfo = '';\n },\n setActiveContact(id) {\n this.activeId = id;\n },\n openChat(contact) {\n if (!contact) {\n return;\n }\n const conversations = (0,_stores_conversations__WEBPACK_IMPORTED_MODULE_8__.useConversationStore)();\n const navigation = (0,_stores_navigation__WEBPACK_IMPORTED_MODULE_9__.useNavigationStore)();\n const conversation = _services_imClient__WEBPACK_IMPORTED_MODULE_7__.imClient.createSingleConversation(contact.uid, contact);\n conversations.upsertConversation(conversation);\n conversations.setActiveConversation(conversation.id);\n navigation.showMessages();\n }\n }\n});\n\n//# sourceURL=webpack://jc-web-im/./src/stores/contacts.js?\n}"); + +/***/ }), + +/***/ "./src/stores/conversations.js": +/*!*************************************!*\ + !*** ./src/stores/conversations.js ***! + \*************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ useConversationStore: function() { return /* binding */ useConversationStore; }\n/* harmony export */ });\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.includes.js */ \"./node_modules/core-js/modules/es.array.includes.js\");\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! core-js/modules/es.array.push.js */ \"./node_modules/core-js/modules/es.array.push.js\");\n/* harmony import */ var core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_push_js__WEBPACK_IMPORTED_MODULE_1__);\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! core-js/modules/es.iterator.constructor.js */ \"./node_modules/core-js/modules/es.iterator.constructor.js\");\n/* harmony import */ var core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_constructor_js__WEBPACK_IMPORTED_MODULE_2__);\n/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! core-js/modules/es.iterator.filter.js */ \"./node_modules/core-js/modules/es.iterator.filter.js\");\n/* harmony import */ var core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_filter_js__WEBPACK_IMPORTED_MODULE_3__);\n/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! core-js/modules/es.iterator.find.js */ \"./node_modules/core-js/modules/es.iterator.find.js\");\n/* harmony import */ var core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_find_js__WEBPACK_IMPORTED_MODULE_4__);\n/* harmony import */ var core_js_modules_es_iterator_flat_map_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! core-js/modules/es.iterator.flat-map.js */ \"./node_modules/core-js/modules/es.iterator.flat-map.js\");\n/* harmony import */ var core_js_modules_es_iterator_flat_map_js__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_flat_map_js__WEBPACK_IMPORTED_MODULE_5__);\n/* harmony import */ var core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! core-js/modules/es.iterator.for-each.js */ \"./node_modules/core-js/modules/es.iterator.for-each.js\");\n/* harmony import */ var core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_for_each_js__WEBPACK_IMPORTED_MODULE_6__);\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! core-js/modules/es.iterator.map.js */ \"./node_modules/core-js/modules/es.iterator.map.js\");\n/* harmony import */ var core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_map_js__WEBPACK_IMPORTED_MODULE_7__);\n/* harmony import */ var core_js_modules_es_iterator_reduce_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! core-js/modules/es.iterator.reduce.js */ \"./node_modules/core-js/modules/es.iterator.reduce.js\");\n/* harmony import */ var core_js_modules_es_iterator_reduce_js__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_reduce_js__WEBPACK_IMPORTED_MODULE_8__);\n/* harmony import */ var core_js_modules_es_iterator_some_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! core-js/modules/es.iterator.some.js */ \"./node_modules/core-js/modules/es.iterator.some.js\");\n/* harmony import */ var core_js_modules_es_iterator_some_js__WEBPACK_IMPORTED_MODULE_9___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_iterator_some_js__WEBPACK_IMPORTED_MODULE_9__);\n/* harmony import */ var core_js_modules_es_map_get_or_insert_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! core-js/modules/es.map.get-or-insert.js */ \"./node_modules/core-js/modules/es.map.get-or-insert.js\");\n/* harmony import */ var core_js_modules_es_map_get_or_insert_js__WEBPACK_IMPORTED_MODULE_10___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_map_get_or_insert_js__WEBPACK_IMPORTED_MODULE_10__);\n/* harmony import */ var core_js_modules_es_map_get_or_insert_computed_js__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! core-js/modules/es.map.get-or-insert-computed.js */ \"./node_modules/core-js/modules/es.map.get-or-insert-computed.js\");\n/* harmony import */ var core_js_modules_es_map_get_or_insert_computed_js__WEBPACK_IMPORTED_MODULE_11___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_map_get_or_insert_computed_js__WEBPACK_IMPORTED_MODULE_11__);\n/* harmony import */ var core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(/*! core-js/modules/es.set.difference.v2.js */ \"./node_modules/core-js/modules/es.set.difference.v2.js\");\n/* harmony import */ var core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_12___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_difference_v2_js__WEBPACK_IMPORTED_MODULE_12__);\n/* harmony import */ var core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(/*! core-js/modules/es.set.intersection.v2.js */ \"./node_modules/core-js/modules/es.set.intersection.v2.js\");\n/* harmony import */ var core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_13___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_intersection_v2_js__WEBPACK_IMPORTED_MODULE_13__);\n/* harmony import */ var core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(/*! core-js/modules/es.set.is-disjoint-from.v2.js */ \"./node_modules/core-js/modules/es.set.is-disjoint-from.v2.js\");\n/* harmony import */ var core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_14___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_disjoint_from_v2_js__WEBPACK_IMPORTED_MODULE_14__);\n/* harmony import */ var core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(/*! core-js/modules/es.set.is-subset-of.v2.js */ \"./node_modules/core-js/modules/es.set.is-subset-of.v2.js\");\n/* harmony import */ var core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_15___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_subset_of_v2_js__WEBPACK_IMPORTED_MODULE_15__);\n/* harmony import */ var core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(/*! core-js/modules/es.set.is-superset-of.v2.js */ \"./node_modules/core-js/modules/es.set.is-superset-of.v2.js\");\n/* harmony import */ var core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_16___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_is_superset_of_v2_js__WEBPACK_IMPORTED_MODULE_16__);\n/* harmony import */ var core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(/*! core-js/modules/es.set.symmetric-difference.v2.js */ \"./node_modules/core-js/modules/es.set.symmetric-difference.v2.js\");\n/* harmony import */ var core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_17___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_symmetric_difference_v2_js__WEBPACK_IMPORTED_MODULE_17__);\n/* harmony import */ var core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(/*! core-js/modules/es.set.union.v2.js */ \"./node_modules/core-js/modules/es.set.union.v2.js\");\n/* harmony import */ var core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_18___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_set_union_v2_js__WEBPACK_IMPORTED_MODULE_18__);\n/* harmony import */ var pinia__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(/*! pinia */ \"./node_modules/pinia/dist/pinia.mjs\");\n/* harmony import */ var _services_imClient__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(/*! @/services/imClient */ \"./src/services/imClient.js\");\n/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(/*! @/config */ \"./src/config.js\");\n/* harmony import */ var _stores_session__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(/*! @/stores/session */ \"./src/stores/session.js\");\n/* harmony import */ var _services_realtimeNotifications__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(/*! @/services/realtimeNotifications */ \"./src/services/realtimeNotifications.js\");\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nconst LOCAL_CONVERSATION_STORAGE_KEY = 'qmt-web-im-local-conversations-v2';\nconst LOCAL_FAVORITE_MESSAGES_KEY = 'qmt-web-im-favorite-messages-v1';\nfunction scopedStorageKey(baseKey) {\n const session = (0,_stores_session__WEBPACK_IMPORTED_MODULE_22__.useSessionStore)();\n const scope = `${_config__WEBPACK_IMPORTED_MODULE_21__.appConfig.appServer || ''}:${session.userId || 'anonymous'}`;\n return `${baseKey}:${encodeURIComponent(scope)}`;\n}\nfunction readFavoriteMessages() {\n try {\n return JSON.parse(localStorage.getItem(scopedStorageKey(LOCAL_FAVORITE_MESSAGES_KEY)) || '[]');\n } catch (error) {\n return [];\n }\n}\nfunction writeFavoriteMessages(messages) {\n localStorage.setItem(scopedStorageKey(LOCAL_FAVORITE_MESSAGES_KEY), JSON.stringify(messages));\n}\nfunction readLocalConversations() {\n try {\n return JSON.parse(localStorage.getItem(scopedStorageKey(LOCAL_CONVERSATION_STORAGE_KEY)) || '[]');\n } catch (error) {\n return [];\n }\n}\nfunction writeLocalConversations(conversations) {\n localStorage.setItem(scopedStorageKey(LOCAL_CONVERSATION_STORAGE_KEY), JSON.stringify(conversations));\n}\nfunction randomId() {\n if (window.crypto && window.crypto.randomUUID) {\n return window.crypto.randomUUID().replaceAll('-', '');\n }\n return `${Date.now()}${Math.random().toString(16).slice(2)}`;\n}\nfunction messageKey(message) {\n return message.id || message.messageUid;\n}\nfunction messageStableKey(message) {\n return String(message?.messageUid || message?.id || '');\n}\nfunction messageActivityTimestamp(message) {\n return Number(message?.activityTimestamp || message?.editedAt || message?.displayTimestamp || message?.timestamp || 0);\n}\nfunction isRecalledMessage(message) {\n return Boolean(message?.recalled) || message?.status === 'recalled';\n}\nfunction attachmentLabel(message) {\n const attachment = message?.attachment;\n if (!attachment) {\n return '';\n }\n if (attachment.type === 'image') {\n return '[图片]';\n }\n return `[文件] ${attachment.name || '文件'}`;\n}\nfunction messagePreviewText(message) {\n if (isRecalledMessage(message)) {\n return `[${message?.recallStatus || '已撤回'}]`;\n }\n const text = String(message?.text || '').replace(/\\s+/g, ' ').trim();\n return text || attachmentLabel(message);\n}\nfunction quotePrefix(message) {\n const text = messagePreviewText(message);\n return text.length > 48 ? `${text.slice(0, 48)}...` : text;\n}\nasync function writeClipboard(text) {\n if (navigator.clipboard?.writeText) {\n await navigator.clipboard.writeText(text);\n return;\n }\n const element = document.createElement('textarea');\n element.value = text;\n element.setAttribute('readonly', 'readonly');\n element.style.position = 'fixed';\n element.style.left = '-9999px';\n document.body.appendChild(element);\n element.select();\n document.execCommand('copy');\n document.body.removeChild(element);\n}\nfunction isSameMessage(left, right) {\n if (left.messageUid && right.messageUid && left.messageUid === right.messageUid) {\n return true;\n }\n if (left.id && right.id && left.id === right.id) {\n return true;\n }\n return left.direction === right.direction && left.text === right.text && (left.type || 'text') === (right.type || 'text') && Math.abs(Number(left.displayTimestamp || left.timestamp || 0) - Number(right.displayTimestamp || right.timestamp || 0)) < 15000;\n}\nfunction hasExactMessage(messages, message) {\n return messages.some(item => {\n if (item.id && message.id && item.id === message.id) {\n return true;\n }\n return item.messageUid && message.messageUid && item.messageUid === message.messageUid;\n });\n}\nfunction shouldUseMessagePolling() {\n return _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.shouldUseMessagePolling ? _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.shouldUseMessagePolling() : true;\n}\nfunction canMarkReadNow(options = {}) {\n if (typeof document === 'undefined') {\n return false;\n }\n if (document.visibilityState !== 'visible') {\n return false;\n }\n if (options.userAction) {\n return true;\n }\n return true;\n}\nfunction normalizeMessageForDisplay(message) {\n if (!message || message.attachment || String(message.text || '').trim()) {\n return message;\n }\n const contentType = Number(message.contentType || 0);\n const labels = {\n 3: '[图片加载失败]',\n 90: '[红包]',\n 105: '[领取了红包]'\n };\n if (!labels[contentType]) {\n return message;\n }\n return {\n ...message,\n type: 'text',\n text: labels[contentType]\n };\n}\nfunction mergeVisibleMessages(currentMessages, syncedMessages) {\n const merged = [...syncedMessages].map(normalizeMessageForDisplay);\n currentMessages.forEach(currentMessage => {\n currentMessage = normalizeMessageForDisplay(currentMessage);\n const exists = merged.some(message => isSameMessage(message, currentMessage));\n const shouldKeepLocal = currentMessage.status === 'sending' || currentMessage.status === 'failed' || currentMessage.direction === 'out' && currentMessage.status === 'sent' && Date.now() - Number(currentMessage.timestamp || 0) < 30000;\n if (!exists && shouldKeepLocal) {\n merged.push(currentMessage);\n }\n });\n return merged.filter((message, index, messages) => {\n const key = messageKey(message);\n return messages.findIndex(item => messageKey(item) === key) === index;\n }).sort((a, b) => Number(a.displayTimestamp || a.timestamp || 0) - Number(b.displayTimestamp || b.timestamp || 0));\n}\nfunction incomingEditedMessages(previousMessages, nextMessages) {\n const previousByKey = new Map();\n (previousMessages || []).forEach(message => {\n const key = messageStableKey(message);\n if (key) {\n previousByKey.set(key, message);\n }\n });\n return (nextMessages || []).filter(message => {\n if (message.direction !== 'in') {\n return false;\n }\n const editedAt = Number(message.editedAt || 0);\n if (editedAt <= 0) {\n return false;\n }\n const previous = previousByKey.get(messageStableKey(message));\n return previous && editedAt > Number(previous.editedAt || 0);\n });\n}\nlet notificationAudioContext = null;\nfunction playNotificationSound() {\n if (typeof window === 'undefined') {\n return;\n }\n try {\n const AudioContext = window.AudioContext || window.webkitAudioContext;\n if (!AudioContext) {\n return;\n }\n notificationAudioContext = notificationAudioContext || new AudioContext();\n if (notificationAudioContext.state === 'suspended') {\n notificationAudioContext.resume().catch(() => {});\n }\n const oscillator = notificationAudioContext.createOscillator();\n const gain = notificationAudioContext.createGain();\n const startAt = notificationAudioContext.currentTime;\n oscillator.type = 'sine';\n oscillator.frequency.setValueAtTime(880, startAt);\n oscillator.frequency.setValueAtTime(660, startAt + 0.1);\n gain.gain.setValueAtTime(0.0001, startAt);\n gain.gain.exponentialRampToValueAtTime(0.12, startAt + 0.02);\n gain.gain.exponentialRampToValueAtTime(0.0001, startAt + 0.22);\n oscillator.connect(gain);\n gain.connect(notificationAudioContext.destination);\n oscillator.start(startAt);\n oscillator.stop(startAt + 0.24);\n } catch (error) {\n // Notification sound is best-effort; browsers may block audio until user interaction.\n }\n}\nconst useConversationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_19__.defineStore)('conversations', {\n state: () => ({\n activeId: '',\n conversations: [],\n messagesByConversation: {},\n visibleMessagesByConversation: {},\n eventsBound: false,\n usingDemoData: false,\n loading: false,\n syncTimer: null,\n conversationSyncTimer: null,\n readPresenceEventsBound: false,\n syncInFlight: false,\n pendingReadReceiptConversationId: '',\n conversationSyncInFlight: false,\n syncAfterSendTimer: null,\n optimisticMessagesByConversation: {},\n quotedMessage: null,\n forwardingMessages: [],\n editingMessage: null,\n editingText: '',\n editingMode: '',\n messageDetailOpen: false,\n messageDetailLoading: false,\n messageDetailError: '',\n messageDetail: null,\n attentionMessageId: '',\n attentionMessageTimer: null,\n selectionMode: false,\n selectedMessageIds: [],\n favoriteMessages: [],\n activeFavoriteKey: '',\n actionNotice: '',\n actionError: '',\n realtimeSyncTimer: null\n }),\n getters: {\n activeConversation(state) {\n return state.conversations.find(item => item.id === state.activeId) || state.conversations[0] || null;\n },\n activeMessages(state) {\n return state.visibleMessagesByConversation[state.activeId] || [];\n },\n totalUnread(state) {\n return state.conversations.reduce((sum, item) => sum + Number(item.unread || 0), 0);\n },\n canSendActiveConversation(state) {\n const conversation = state.conversations.find(item => item.id === state.activeId) || state.conversations[0] || null;\n if (!conversation) {\n return false;\n }\n if (conversation.type !== 'channel') {\n return true;\n }\n return ['owner', 'admin'].includes(conversation.role || conversation.raw?.role || '');\n },\n favoriteMessageIds(state) {\n return state.favoriteMessages.map(message => message.key);\n },\n activeFavorite(state) {\n return state.favoriteMessages.find(message => message.key === state.activeFavoriteKey) || state.favoriteMessages[0] || null;\n },\n selectedMessages(state) {\n const selectedIds = new Set(state.selectedMessageIds);\n return (state.visibleMessagesByConversation[state.activeId] || []).filter(message => selectedIds.has(messageStableKey(message)));\n }\n },\n actions: {\n bootstrapDemoData() {\n const session = (0,_stores_session__WEBPACK_IMPORTED_MODULE_22__.useSessionStore)();\n this.bindImEvents();\n this.usingDemoData = false;\n if (!session.userId) {\n this.clearUserState();\n return;\n }\n this.loadScopedLocalState();\n this.startMessageSync();\n _services_realtimeNotifications__WEBPACK_IMPORTED_MODULE_23__.realtimeNotifications.start(session.userId, payload => this.handleRealtimeNotification(payload));\n },\n loadScopedLocalState() {\n this.conversations = [];\n this.favoriteMessages = readFavoriteMessages();\n this.activeFavoriteKey = this.favoriteMessages[0]?.key || '';\n this.activeId = '';\n this.messagesByConversation = {};\n this.visibleMessagesByConversation = {};\n this.optimisticMessagesByConversation = {};\n this.quotedMessage = null;\n this.forwardingMessages = [];\n this.editingMessage = null;\n this.editingText = '';\n this.editingMode = '';\n this.messageDetailOpen = false;\n this.messageDetailLoading = false;\n this.messageDetailError = '';\n this.messageDetail = null;\n this.selectionMode = false;\n this.selectedMessageIds = [];\n },\n clearUserState() {\n window.clearInterval(this.syncTimer);\n window.clearInterval(this.conversationSyncTimer);\n window.clearTimeout(this.syncAfterSendTimer);\n window.clearTimeout(this.attentionMessageTimer);\n window.clearTimeout(this.realtimeSyncTimer);\n _services_realtimeNotifications__WEBPACK_IMPORTED_MODULE_23__.realtimeNotifications.stop();\n this.syncTimer = null;\n this.conversationSyncTimer = null;\n this.syncAfterSendTimer = null;\n this.attentionMessageTimer = null;\n this.realtimeSyncTimer = null;\n this.syncInFlight = false;\n this.conversationSyncInFlight = false;\n this.pendingReadReceiptConversationId = '';\n this.activeId = '';\n this.conversations = [];\n this.messagesByConversation = {};\n this.visibleMessagesByConversation = {};\n this.optimisticMessagesByConversation = {};\n this.quotedMessage = null;\n this.forwardingMessages = [];\n this.editingMessage = null;\n this.editingText = '';\n this.editingMode = '';\n this.messageDetailOpen = false;\n this.messageDetailLoading = false;\n this.messageDetailError = '';\n this.messageDetail = null;\n this.attentionMessageId = '';\n this.selectionMode = false;\n this.selectedMessageIds = [];\n this.favoriteMessages = [];\n this.activeFavoriteKey = '';\n this.actionNotice = '';\n this.actionError = '';\n },\n bindImEvents() {\n if (this.eventsBound) {\n return;\n }\n this.eventsBound = true;\n _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.on('messageSent', message => {\n this.appendMessage(message.conversationId, message);\n });\n _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.on('messageReceived', message => {\n this.appendMessage(message.conversationId, message);\n if (message.conversationId === this.activeId) {\n this.scheduleActiveMessagesSync({\n allowReadReceipt: true\n }, 500);\n }\n });\n _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.on('messageStatusChanged', message => {\n this.replaceMessage(message.conversationId, message);\n });\n _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.on('conversationsChanged', remoteConversations => {\n this.usingDemoData = false;\n this.applyRemoteConversations(remoteConversations);\n });\n },\n setActiveConversation(id) {\n this.activeId = id;\n const conversation = this.conversations.find(item => item.id === id);\n if (conversation) {\n conversation.unread = 0;\n }\n this.loadActiveMessages({\n allowReadReceipt: true,\n userAction: true\n });\n },\n sendText(text) {\n const conversation = this.activeConversation;\n if (!conversation || !text.trim()) {\n return;\n }\n let value = text.trim();\n if (this.quotedMessage) {\n value = `引用:${quotePrefix(this.quotedMessage)}\\n${value}`;\n this.quotedMessage = null;\n }\n const message = {\n id: randomId(),\n conversationId: conversation.id,\n from: 'me',\n direction: 'out',\n type: 'text',\n text: value,\n status: 'sending',\n deliveryStatus: 'sending',\n timestamp: Date.now(),\n displayTimestamp: Date.now()\n };\n this.appendVisibleMessage(conversation.id, message);\n this.appendMessage(conversation.id, message);\n _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.sendTextMessage(conversation, value, message);\n conversation.subtitle = value;\n conversation.updatedAt = Date.now();\n writeLocalConversations(this.conversations);\n window.clearTimeout(this.syncAfterSendTimer);\n this.syncAfterSendTimer = window.setTimeout(() => this.syncActiveMessages(), 250);\n },\n sendAttachment({\n type,\n file,\n text = ''\n }) {\n const conversation = this.activeConversation;\n if (!conversation || !file || !['image', 'file'].includes(type)) {\n return;\n }\n let value = text.trim();\n if (this.quotedMessage) {\n value = `引用:${quotePrefix(this.quotedMessage)}\\n${value}`.trim();\n this.quotedMessage = null;\n }\n const now = Date.now();\n const message = {\n id: randomId(),\n conversationId: conversation.id,\n from: 'me',\n senderRole: conversation.role || conversation.raw?.role || '',\n direction: 'out',\n type,\n text: value,\n attachment: {\n type,\n name: file.name || (type === 'image' ? '图片' : '文件'),\n mime: file.type || '',\n size: file.size || 0,\n url: type === 'image' ? URL.createObjectURL(file) : ''\n },\n status: 'sending',\n deliveryStatus: 'sending',\n timestamp: now,\n displayTimestamp: now\n };\n this.appendVisibleMessage(conversation.id, message);\n this.appendMessage(conversation.id, message);\n _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.sendAttachmentMessage(conversation, {\n type,\n file,\n text: value\n }, message);\n conversation.subtitle = messagePreviewText(message);\n conversation.updatedAt = now;\n writeLocalConversations(this.conversations);\n window.clearTimeout(this.syncAfterSendTimer);\n this.syncAfterSendTimer = window.setTimeout(() => this.syncActiveMessages(), 350);\n },\n async sendTextToConversation(conversation, text) {\n if (!conversation || !text.trim()) {\n return null;\n }\n const value = text.trim();\n const message = {\n id: randomId(),\n conversationId: conversation.id,\n from: 'me',\n direction: 'out',\n type: 'text',\n text: value,\n status: 'sending',\n deliveryStatus: 'sending',\n timestamp: Date.now(),\n displayTimestamp: Date.now()\n };\n this.appendMessage(conversation.id, message);\n const sentMessage = await _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.sendTextMessage(conversation, value, message);\n this.replaceMessage(conversation.id, sentMessage);\n return sentMessage;\n },\n upsertConversation(conversation) {\n const index = this.conversations.findIndex(item => item.id === conversation.id);\n if (index >= 0) {\n this.conversations.splice(index, 1, {\n ...this.conversations[index],\n ...conversation\n });\n } else {\n this.conversations.unshift(conversation);\n }\n this.usingDemoData = false;\n writeLocalConversations(this.conversations);\n },\n removeConversation(conversationId) {\n this.conversations = this.conversations.filter(conversation => conversation.id !== conversationId);\n const {\n [conversationId]: removedMessages,\n ...messagesByConversation\n } = this.messagesByConversation;\n const {\n [conversationId]: removedVisibleMessages,\n ...visibleMessagesByConversation\n } = this.visibleMessagesByConversation;\n const {\n [conversationId]: removedOptimisticMessages,\n ...optimisticMessagesByConversation\n } = this.optimisticMessagesByConversation;\n void removedMessages;\n void removedVisibleMessages;\n void removedOptimisticMessages;\n this.messagesByConversation = messagesByConversation;\n this.visibleMessagesByConversation = visibleMessagesByConversation;\n this.optimisticMessagesByConversation = optimisticMessagesByConversation;\n if (this.activeId === conversationId) {\n this.activeId = this.conversations[0]?.id || '';\n }\n writeLocalConversations(this.conversations);\n if (this.activeId) {\n this.loadActiveMessages();\n }\n },\n applyRemoteConversations(remoteConversations) {\n const remote = Array.isArray(remoteConversations) ? remoteConversations : [];\n if (!remote.length && this.conversations.length) {\n return;\n }\n const merged = new Map();\n this.conversations.forEach(conversation => {\n merged.set(conversation.id, conversation);\n });\n remote.forEach(conversation => {\n const unread = conversation.id === this.activeId && canMarkReadNow() ? 0 : Number(conversation.unread || 0);\n merged.set(conversation.id, {\n ...(merged.get(conversation.id) || {}),\n ...conversation,\n unread\n });\n });\n this.conversations = Array.from(merged.values()).sort((a, b) => Number(b.updatedAt || 0) - Number(a.updatedAt || 0)).slice(0, 100);\n if (!this.conversations.length) {\n this.activeId = '';\n this.messagesByConversation = {};\n this.visibleMessagesByConversation = {};\n this.optimisticMessagesByConversation = {};\n writeLocalConversations(this.conversations);\n return;\n }\n if (!this.conversations.find(item => item.id === this.activeId)) {\n this.activeId = this.conversations[0].id;\n }\n writeLocalConversations(this.conversations);\n this.loadActiveMessages();\n },\n appendMessage(conversationId, message) {\n if (!this.messagesByConversation[conversationId]) {\n this.messagesByConversation = {\n ...this.messagesByConversation,\n [conversationId]: []\n };\n }\n if (message.direction === 'out' && (message.status === 'sending' || message.status === 'sent')) {\n this.keepOptimisticMessage(conversationId, message);\n }\n if (!hasExactMessage(this.messagesByConversation[conversationId], message)) {\n this.messagesByConversation = {\n ...this.messagesByConversation,\n [conversationId]: [...this.messagesByConversation[conversationId], message]\n };\n }\n this.updateConversationFromMessage(conversationId, message);\n this.syncVisibleMessages(conversationId);\n writeLocalConversations(this.conversations);\n },\n removeMessages(conversationId, messages) {\n const ids = new Set(messages.flatMap(message => [message.id, message.messageUid].filter(Boolean).map(String)));\n const filterMessages = list => (list || []).filter(message => {\n return ![message.id, message.messageUid].filter(Boolean).some(id => ids.has(String(id)));\n });\n this.messagesByConversation = {\n ...this.messagesByConversation,\n [conversationId]: filterMessages(this.messagesByConversation[conversationId])\n };\n this.visibleMessagesByConversation = {\n ...this.visibleMessagesByConversation,\n [conversationId]: filterMessages(this.visibleMessagesByConversation[conversationId])\n };\n this.optimisticMessagesByConversation = {\n ...this.optimisticMessagesByConversation,\n [conversationId]: filterMessages(this.optimisticMessagesByConversation[conversationId])\n };\n this.selectedMessageIds = this.selectedMessageIds.filter(id => !ids.has(String(id)));\n },\n replaceLocalMessage(conversationId, targetMessage, nextMessage) {\n const keys = new Set([targetMessage.id, targetMessage.messageUid].filter(Boolean).map(String));\n const replaceInList = list => (list || []).map(message => {\n const matched = [message.id, message.messageUid].filter(Boolean).some(id => keys.has(String(id)));\n return matched ? {\n ...message,\n ...nextMessage\n } : message;\n });\n this.messagesByConversation = {\n ...this.messagesByConversation,\n [conversationId]: replaceInList(this.messagesByConversation[conversationId])\n };\n this.visibleMessagesByConversation = {\n ...this.visibleMessagesByConversation,\n [conversationId]: replaceInList(this.visibleMessagesByConversation[conversationId])\n };\n },\n updateConversationFromMessage(conversationId, message) {\n const conversation = this.conversations.find(item => item.id === conversationId);\n if (!conversation) {\n return;\n }\n conversation.subtitle = message.text || conversation.subtitle || '';\n conversation.updatedAt = messageActivityTimestamp(message) || Date.now();\n if (message.direction === 'in') {\n conversation.unread = conversationId === this.activeId ? 0 : Number(conversation.unread || 0) + 1;\n }\n this.conversations = [...this.conversations].sort((a, b) => Number(b.updatedAt || 0) - Number(a.updatedAt || 0));\n },\n appendVisibleMessage(conversationId, message) {\n const messages = this.visibleMessagesByConversation[conversationId] || [];\n this.visibleMessagesByConversation = {\n ...this.visibleMessagesByConversation,\n [conversationId]: [...messages.filter(item => item.id !== message.id), message].sort((a, b) => Number(a.displayTimestamp || a.timestamp || 0) - Number(b.displayTimestamp || b.timestamp || 0))\n };\n },\n replaceMessage(conversationId, message) {\n if (!this.messagesByConversation[conversationId]) {\n this.messagesByConversation = {\n ...this.messagesByConversation,\n [conversationId]: []\n };\n }\n if (message.direction === 'out' && (message.status === 'sending' || message.status === 'sent' || message.status === 'failed')) {\n this.keepOptimisticMessage(conversationId, message);\n }\n const index = this.messagesByConversation[conversationId].findIndex(item => item.id === message.id);\n if (index >= 0) {\n const messages = [...this.messagesByConversation[conversationId]];\n messages.splice(index, 1, message);\n this.messagesByConversation = {\n ...this.messagesByConversation,\n [conversationId]: messages\n };\n } else {\n this.messagesByConversation = {\n ...this.messagesByConversation,\n [conversationId]: [...this.messagesByConversation[conversationId], message]\n };\n }\n this.syncVisibleMessages(conversationId);\n },\n syncVisibleMessages(conversationId) {\n this.visibleMessagesByConversation = {\n ...this.visibleMessagesByConversation,\n [conversationId]: mergeVisibleMessages(this.optimisticMessagesByConversation[conversationId] || [], this.messagesByConversation[conversationId] || [])\n };\n },\n keepOptimisticMessage(conversationId, message) {\n if (!this.optimisticMessagesByConversation[conversationId]) {\n this.optimisticMessagesByConversation = {\n ...this.optimisticMessagesByConversation,\n [conversationId]: []\n };\n }\n const messages = this.optimisticMessagesByConversation[conversationId];\n const index = messages.findIndex(item => item.id === message.id);\n let nextMessages;\n if (index >= 0) {\n nextMessages = [...messages];\n nextMessages.splice(index, 1, message);\n } else {\n nextMessages = [...messages, message];\n }\n this.optimisticMessagesByConversation = {\n ...this.optimisticMessagesByConversation,\n [conversationId]: nextMessages\n };\n },\n pruneOptimisticMessages(conversationId, syncedMessages) {\n const messages = this.optimisticMessagesByConversation[conversationId] || [];\n if (!messages.length) {\n return;\n }\n this.optimisticMessagesByConversation = {\n ...this.optimisticMessagesByConversation,\n [conversationId]: messages.filter(message => {\n const remoteMatched = syncedMessages.some(syncedMessage => {\n return syncedMessage.id !== message.id && isSameMessage(syncedMessage, message);\n });\n if (remoteMatched && message.status !== 'failed') {\n return false;\n }\n return Date.now() - Number(message.displayTimestamp || message.timestamp || 0) < 30000 || message.status === 'failed';\n })\n };\n },\n loadActiveMessages(options = {}) {\n const syncOptions = typeof options === 'boolean' ? {\n allowReadReceipt: options\n } : options;\n const conversation = this.activeConversation;\n if (!conversation) {\n return;\n }\n this.loading = true;\n const messages = _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.getMessages(conversation, 30);\n this.pruneOptimisticMessages(conversation.id, messages);\n this.messagesByConversation = {\n ...this.messagesByConversation,\n [conversation.id]: mergeVisibleMessages(this.messagesByConversation[conversation.id] || [], messages)\n };\n this.syncVisibleMessages(conversation.id);\n this.loading = false;\n this.syncActiveMessages(syncOptions);\n },\n async syncActiveMessages(options = {}) {\n const conversation = this.activeConversation;\n if (!conversation) {\n return;\n }\n const wantsReadReceipt = Boolean(options.allowReadReceipt);\n const explicitReadReceipt = wantsReadReceipt && canMarkReadNow({\n userAction: Boolean(options.userAction)\n });\n const pollingReadReceipt = canMarkReadNow();\n const markRead = explicitReadReceipt || pollingReadReceipt;\n if (this.syncInFlight) {\n if (wantsReadReceipt) {\n this.pendingReadReceiptConversationId = conversation.id;\n }\n return;\n }\n this.syncInFlight = true;\n const syncedConversationId = conversation.id;\n const previousMessages = this.messagesByConversation[syncedConversationId] || [];\n try {\n const messages = await _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.fetchRemoteMessages(conversation, 50, {\n markRead,\n includeConversations: Boolean(options.includeConversations)\n });\n const editedIncomingMessages = incomingEditedMessages(previousMessages, messages);\n this.pruneOptimisticMessages(syncedConversationId, messages);\n this.messagesByConversation = {\n ...this.messagesByConversation,\n [syncedConversationId]: mergeVisibleMessages(this.messagesByConversation[syncedConversationId] || [], messages)\n };\n this.syncVisibleMessages(syncedConversationId);\n if (editedIncomingMessages.length && syncedConversationId === this.activeId) {\n this.focusMessage(editedIncomingMessages.sort((a, b) => messageActivityTimestamp(b) - messageActivityTimestamp(a))[0]);\n playNotificationSound();\n }\n const latest = messages.reduce((latestMessage, message) => {\n if (!latestMessage || messageActivityTimestamp(message) > messageActivityTimestamp(latestMessage)) {\n return message;\n }\n return latestMessage;\n }, null);\n if (latest) {\n conversation.subtitle = messagePreviewText(latest);\n conversation.updatedAt = messageActivityTimestamp(latest);\n writeLocalConversations(this.conversations);\n }\n } catch (error) {\n // Keep the local view usable if a polling request misses.\n } finally {\n this.syncInFlight = false;\n if (this.pendingReadReceiptConversationId === this.activeId && canMarkReadNow()) {\n this.pendingReadReceiptConversationId = '';\n window.setTimeout(() => this.syncActiveMessages({\n allowReadReceipt: true\n }), 0);\n }\n }\n },\n scheduleActiveMessagesSync(options = {}, delay = 500) {\n if (!this.activeId || typeof document !== 'undefined' && document.visibilityState !== 'visible') {\n return;\n }\n window.clearTimeout(this.syncAfterSendTimer);\n this.syncAfterSendTimer = window.setTimeout(() => {\n this.syncAfterSendTimer = null;\n this.syncActiveMessages(options);\n }, Math.max(0, Number(delay) || 0));\n },\n startMessageSync() {\n this.bindReadPresenceEvents();\n if (!this.syncTimer) {\n this.syncTimer = window.setInterval(() => {\n if (document.visibilityState === 'visible' && !_services_realtimeNotifications__WEBPACK_IMPORTED_MODULE_23__.realtimeNotifications.isConnected()) {\n this.syncActiveMessages();\n }\n }, Math.max(250, _config__WEBPACK_IMPORTED_MODULE_21__.appConfig.syncIntervalMs));\n }\n if (!this.conversationSyncTimer) {\n this.conversationSyncTimer = window.setInterval(() => {\n if (document.visibilityState === 'visible' && !_services_realtimeNotifications__WEBPACK_IMPORTED_MODULE_23__.realtimeNotifications.isConnected()) {\n this.refreshRemoteConversations();\n }\n }, Math.max(5000, _config__WEBPACK_IMPORTED_MODULE_21__.appConfig.syncIntervalMs * 2));\n this.refreshRemoteConversations();\n }\n },\n handleRealtimeNotification() {\n window.clearTimeout(this.realtimeSyncTimer);\n this.realtimeSyncTimer = window.setTimeout(() => {\n this.refreshRemoteConversations();\n this.scheduleActiveMessagesSync({\n allowReadReceipt: canMarkReadNow(),\n includeConversations: true\n }, 300);\n }, 300);\n },\n bindReadPresenceEvents() {\n if (this.readPresenceEventsBound || typeof window === 'undefined' || typeof document === 'undefined') {\n return;\n }\n this.readPresenceEventsBound = true;\n const syncWhenReadable = () => {\n if (canMarkReadNow()) {\n this.refreshRemoteConversations();\n this.scheduleActiveMessagesSync({\n allowReadReceipt: true\n }, 200);\n }\n };\n window.addEventListener('focus', syncWhenReadable);\n document.addEventListener('visibilitychange', syncWhenReadable);\n },\n async refreshRemoteConversations() {\n this.bindImEvents();\n this.usingDemoData = false;\n if (this.conversationSyncInFlight) {\n return;\n }\n this.conversationSyncInFlight = true;\n try {\n const remoteConversations = await _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.fetchRemoteConversations(50);\n this.applyRemoteConversations(remoteConversations);\n } catch (error) {\n _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.refreshConversationList();\n } finally {\n this.conversationSyncInFlight = false;\n }\n },\n setActionNotice(message) {\n this.actionNotice = message;\n this.actionError = '';\n },\n setActionError(message) {\n this.actionError = message;\n this.actionNotice = '';\n },\n focusMessage(message) {\n const key = messageStableKey(message);\n if (!key) {\n return;\n }\n this.attentionMessageId = key;\n window.clearTimeout(this.attentionMessageTimer);\n this.attentionMessageTimer = window.setTimeout(() => {\n if (this.attentionMessageId === key) {\n this.attentionMessageId = '';\n }\n }, 2800);\n },\n async handleMessageAction({\n action,\n message\n }) {\n if (!message) {\n return;\n }\n if (action === 'copy') {\n await this.copyMessages([message]);\n } else if (action === 'edit') {\n this.startEditMessage(message);\n } else if (action === 'delete') {\n this.deleteMessages([message]);\n } else if (action === 'forward') {\n this.startForward([message]);\n } else if (action === 'favorite') {\n this.toggleFavorite(message);\n } else if (action === 'detail') {\n await this.openMessageDetail(message);\n } else if (action === 'quote') {\n this.quoteMessage(message);\n } else if (action === 'select') {\n this.enterSelectionMode(message);\n } else if (action === 'recall') {\n await this.recallMessage(message);\n }\n },\n async openMessageDetail(message) {\n const conversation = this.activeConversation;\n if (!conversation || !message) {\n return;\n }\n const key = messageStableKey(message);\n this.messageDetailOpen = true;\n this.messageDetailLoading = true;\n this.messageDetailError = '';\n this.messageDetail = {\n messageUid: key,\n conversationType: conversation.type || conversation.raw?.type || 'single',\n sentAt: Number(message.displayTimestamp || message.timestamp || 0),\n editedAt: Number(message.editedAt || 0),\n recalled: isRecalledMessage(message),\n localOnly: true,\n sender: {\n uid: message.from || '',\n name: message.senderName || message.from || ''\n }\n };\n try {\n if (!/^\\d+$/.test(key)) {\n this.messageDetailError = '本地临时消息暂无送达/已读详情';\n return;\n }\n const detail = await _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.getMessageDetail(conversation, message);\n this.messageDetail = detail || this.messageDetail;\n } catch (error) {\n this.messageDetailError = error.message || '消息详情加载失败';\n } finally {\n this.messageDetailLoading = false;\n }\n },\n closeMessageDetail() {\n this.messageDetailOpen = false;\n this.messageDetailLoading = false;\n this.messageDetailError = '';\n this.messageDetail = null;\n },\n async copyMessages(messages) {\n const text = messages.map(message => message.text || '').join('\\n');\n if (!text.trim()) {\n return;\n }\n try {\n await writeClipboard(text);\n this.setActionNotice(messages.length > 1 ? '已复制所选消息' : '已复制');\n } catch (error) {\n this.setActionError('复制失败');\n }\n },\n deleteMessages(messages) {\n const conversation = this.activeConversation;\n if (!conversation || !messages.length) {\n return;\n }\n _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.hideMessages(conversation, messages);\n this.removeMessages(conversation.id, messages);\n if (!this.selectedMessageIds.length) {\n this.selectionMode = false;\n }\n this.setActionNotice(messages.length > 1 ? '已删除所选消息' : '已删除');\n },\n toggleFavorite(message) {\n const key = messageStableKey(message);\n if (!key) {\n return;\n }\n const index = this.favoriteMessages.findIndex(item => item.key === key);\n if (index >= 0) {\n this.favoriteMessages.splice(index, 1);\n if (this.activeFavoriteKey === key) {\n this.activeFavoriteKey = this.favoriteMessages[0]?.key || '';\n }\n this.setActionNotice('已取消收藏');\n } else {\n const conversation = this.conversations.find(item => item.id === message.conversationId) || {};\n this.favoriteMessages.unshift({\n key,\n id: message.id,\n messageUid: message.messageUid,\n conversationId: message.conversationId,\n conversationTitle: conversation.title || message.conversationTitle || '',\n type: message.type || 'text',\n text: message.text || '',\n preview: messagePreviewText(message),\n senderName: message.senderName || message.from || '',\n timestamp: Number(message.displayTimestamp || message.timestamp || Date.now()),\n editedAt: Number(message.editedAt || 0),\n recalled: isRecalledMessage(message),\n recallStatus: message.recallStatus || (isRecalledMessage(message) ? '已撤回' : ''),\n attachment: message.attachment ? {\n ...message.attachment\n } : null\n });\n this.activeFavoriteKey = key;\n this.setActionNotice('已收藏');\n }\n this.favoriteMessages = this.favoriteMessages.slice(0, 200);\n writeFavoriteMessages(this.favoriteMessages);\n },\n setActiveFavorite(key) {\n this.activeFavoriteKey = String(key || '');\n },\n removeFavorite(key) {\n const value = String(key || '');\n if (!value) {\n return;\n }\n this.favoriteMessages = this.favoriteMessages.filter(message => message.key !== value);\n if (this.activeFavoriteKey === value) {\n this.activeFavoriteKey = this.favoriteMessages[0]?.key || '';\n }\n writeFavoriteMessages(this.favoriteMessages);\n },\n quoteMessage(message) {\n this.quotedMessage = {\n ...message,\n quoteText: quotePrefix(message)\n };\n this.setActionNotice('已引用,输入后发送');\n },\n cancelQuote() {\n this.quotedMessage = null;\n },\n enterSelectionMode(message = null) {\n this.selectionMode = true;\n if (message) {\n const key = messageStableKey(message);\n this.selectedMessageIds = key ? [key] : [];\n }\n },\n toggleSelectedMessage(message) {\n const key = messageStableKey(message);\n if (!key) {\n return;\n }\n const selected = new Set(this.selectedMessageIds);\n if (selected.has(key)) {\n selected.delete(key);\n } else {\n selected.add(key);\n }\n this.selectedMessageIds = Array.from(selected);\n this.selectionMode = this.selectedMessageIds.length > 0;\n },\n clearSelection() {\n this.selectionMode = false;\n this.selectedMessageIds = [];\n },\n copySelectedMessages() {\n return this.copyMessages(this.selectedMessages);\n },\n deleteSelectedMessages() {\n this.deleteMessages(this.selectedMessages);\n },\n startForward(messages) {\n this.forwardingMessages = messages.filter(message => message?.text);\n if (!this.forwardingMessages.length) {\n this.setActionError('没有可转发的消息');\n }\n },\n startForwardSelectedMessages() {\n this.startForward(this.selectedMessages);\n },\n cancelForward() {\n this.forwardingMessages = [];\n },\n startEditMessage(message) {\n const isResend = isRecalledMessage(message);\n if (!isResend && !message?.text) {\n this.setActionError('没有可编辑的文字内容');\n return;\n }\n this.editingMessage = message;\n this.editingText = message.text || '';\n this.editingMode = isResend ? 'resend' : 'edit';\n },\n cancelEditMessage() {\n this.editingMessage = null;\n this.editingText = '';\n this.editingMode = '';\n },\n async completeEditMessage() {\n const conversation = this.activeConversation;\n const message = this.editingMessage;\n const text = this.editingText.trim();\n const isResend = this.editingMode === 'resend' || isRecalledMessage(message);\n if (!conversation || !message) {\n this.cancelEditMessage();\n return;\n }\n if (!text) {\n this.setActionError('编辑内容不能为空');\n return;\n }\n if (!isResend && text === String(message.text || '').trim()) {\n this.cancelEditMessage();\n return;\n }\n try {\n if (isResend) {\n await this.sendTextToConversation(conversation, text);\n this.cancelEditMessage();\n this.setActionNotice('已重新发送');\n return;\n }\n const editedMessage = await _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.editMessage(conversation, message, text);\n this.replaceLocalMessage(conversation.id, message, editedMessage);\n if (messageActivityTimestamp(editedMessage) >= Number(conversation.updatedAt || 0)) {\n conversation.subtitle = editedMessage.text;\n conversation.updatedAt = messageActivityTimestamp(editedMessage);\n writeLocalConversations(this.conversations);\n }\n this.cancelEditMessage();\n this.setActionNotice('已编辑');\n } catch (error) {\n this.setActionError(error.message || '编辑失败');\n }\n },\n async completeForward(targetConversationId) {\n const target = this.conversations.find(conversation => conversation.id === targetConversationId);\n if (!target || !this.forwardingMessages.length) {\n return;\n }\n const text = this.forwardingMessages.map(message => `转发:${message.text || ''}`).join('\\n\\n');\n try {\n await this.sendTextToConversation(target, text);\n this.forwardingMessages = [];\n this.clearSelection();\n this.setActionNotice('已转发');\n } catch (error) {\n this.setActionError(error.message || '转发失败');\n }\n },\n async recallMessage(message) {\n const conversation = this.activeConversation;\n if (!conversation) {\n return;\n }\n try {\n await _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.recallMessage(conversation, message);\n if (message.direction !== 'out') {\n this.removeMessages(conversation.id, [message]);\n this.setActionNotice('已撤回');\n this.syncActiveMessages();\n return;\n }\n this.replaceLocalMessage(conversation.id, message, {\n status: 'recalled',\n deliveryStatus: '',\n recalled: true,\n recallStatus: '已撤回'\n });\n this.setActionNotice('已撤回');\n } catch (error) {\n this.setActionError(error.message || '撤回失败');\n }\n },\n async createGroup(options = {}) {\n const session = (0,_stores_session__WEBPACK_IMPORTED_MODULE_22__.useSessionStore)();\n if (!session.userId) {\n this.setActionError('请先登录');\n return false;\n }\n try {\n const conversation = await _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.createGroup(session.userId, options);\n this.upsertConversation(conversation);\n this.setActiveConversation(conversation.id);\n this.setActionNotice(options.channel ? '公告群已创建' : '群组已创建');\n this.refreshRemoteConversations();\n return true;\n } catch (error) {\n this.setActionError(error.message || (options.channel ? '创建公告群失败' : '创建群组失败'));\n return false;\n }\n },\n async joinGroup(groupKey) {\n const session = (0,_stores_session__WEBPACK_IMPORTED_MODULE_22__.useSessionStore)();\n if (!session.userId) {\n this.setActionError('请先登录');\n return false;\n }\n try {\n const conversation = await _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.joinGroup(session.userId, groupKey);\n this.upsertConversation(conversation);\n this.setActiveConversation(conversation.id);\n this.setActionNotice('已加入群组');\n this.refreshRemoteConversations();\n return true;\n } catch (error) {\n this.setActionError(error.message || '加入群组失败');\n return false;\n }\n },\n async broadcastMessage(targetIds, text) {\n const session = (0,_stores_session__WEBPACK_IMPORTED_MODULE_22__.useSessionStore)();\n if (!session.userId) {\n this.setActionError('请先登录');\n return false;\n }\n try {\n const result = await _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.broadcastMessage(session.userId, targetIds, text);\n const successCount = Number(result.successCount || 0);\n const failedCount = Number(result.failedCount || 0);\n this.setActionNotice(failedCount > 0 ? `已群发 ${successCount} 条,失败 ${failedCount} 条` : `已群发 ${successCount} 条`);\n this.refreshRemoteConversations();\n return true;\n } catch (error) {\n this.setActionError(error.message || '群发失败');\n return false;\n }\n },\n async leaveGroup(conversation) {\n const session = (0,_stores_session__WEBPACK_IMPORTED_MODULE_22__.useSessionStore)();\n if (!session.userId || !conversation?.target) {\n this.setActionError('缺少群组信息');\n return false;\n }\n try {\n await _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.leaveGroup(session.userId, conversation.target);\n this.removeConversation(conversation.id);\n this.setActionNotice('已退出群组');\n this.refreshRemoteConversations();\n return true;\n } catch (error) {\n this.setActionError(error.message || '退出群组失败');\n return false;\n }\n },\n async dismissGroup(conversation) {\n const session = (0,_stores_session__WEBPACK_IMPORTED_MODULE_22__.useSessionStore)();\n if (!session.userId || !conversation?.target) {\n this.setActionError('缺少群组信息');\n return false;\n }\n try {\n await _services_imClient__WEBPACK_IMPORTED_MODULE_20__.imClient.dismissGroup(session.userId, conversation.target);\n this.removeConversation(conversation.id);\n this.setActionNotice('群组已解散');\n this.refreshRemoteConversations();\n return true;\n } catch (error) {\n this.setActionError(error.message || '解散群组失败');\n return false;\n }\n }\n }\n});\n\n//# sourceURL=webpack://jc-web-im/./src/stores/conversations.js?\n}"); + +/***/ }), + +/***/ "./src/stores/navigation.js": +/*!**********************************!*\ + !*** ./src/stores/navigation.js ***! + \**********************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ useNavigationStore: function() { return /* binding */ useNavigationStore; }\n/* harmony export */ });\n/* harmony import */ var pinia__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! pinia */ \"./node_modules/pinia/dist/pinia.mjs\");\n\nconst useNavigationStore = (0,pinia__WEBPACK_IMPORTED_MODULE_0__.defineStore)('navigation', {\n state: () => ({\n section: 'messages'\n }),\n actions: {\n showMessages() {\n this.section = 'messages';\n },\n showContacts() {\n this.section = 'contacts';\n },\n showFavorites() {\n this.section = 'favorites';\n }\n }\n});\n\n//# sourceURL=webpack://jc-web-im/./src/stores/navigation.js?\n}"); + +/***/ }), + +/***/ "./src/stores/session.js": +/*!*******************************!*\ + !*** ./src/stores/session.js ***! + \*******************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ useSessionStore: function() { return /* binding */ useSessionStore; }\n/* harmony export */ });\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core-js/modules/es.array.includes.js */ \"./node_modules/core-js/modules/es.array.includes.js\");\n/* harmony import */ var core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(core_js_modules_es_array_includes_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var pinia__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! pinia */ \"./node_modules/pinia/dist/pinia.mjs\");\n/* harmony import */ var _services_imClient__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @/services/imClient */ \"./src/services/imClient.js\");\n/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! @/config */ \"./src/config.js\");\n\n\n\n\nconst STORAGE_KEY = 'qmt-web-im-session';\nfunction normalizeStoredPortrait(portrait) {\n return portrait || '';\n}\nfunction nextProfilePortrait(profilePortrait, currentPortrait) {\n return normalizeStoredPortrait(profilePortrait) || normalizeStoredPortrait(currentPortrait);\n}\nfunction inferAccountType(account) {\n return String(account || '').includes('@') ? 2 : 1;\n}\nconst useSessionStore = (0,pinia__WEBPACK_IMPORTED_MODULE_1__.defineStore)('session', {\n state: () => ({\n isAuthenticated: false,\n connecting: false,\n connected: false,\n connectionStatusText: '未连接',\n userId: '',\n token: '',\n displayName: '',\n portrait: '',\n error: '',\n sdkVersion: '',\n eventsBound: false\n }),\n actions: {\n restore() {\n const raw = localStorage.getItem(STORAGE_KEY);\n if (!raw) {\n return;\n }\n try {\n const session = JSON.parse(raw);\n if (session.appServer !== _config__WEBPACK_IMPORTED_MODULE_3__.appConfig.appServer) {\n localStorage.removeItem(STORAGE_KEY);\n return;\n }\n if (session.userId && session.token) {\n if (!session.clientId) {\n localStorage.removeItem(STORAGE_KEY);\n return;\n }\n this.userId = session.userId;\n this.token = session.token;\n this.displayName = session.displayName || session.userId;\n this.portrait = normalizeStoredPortrait(session.portrait);\n this.isAuthenticated = true;\n _services_imClient__WEBPACK_IMPORTED_MODULE_2__.imClient.restoreClientId(session.clientId);\n this.connect();\n this.refreshProfile();\n }\n } catch (error) {\n localStorage.removeItem(STORAGE_KEY);\n }\n },\n async loginWithPassword(account, password, type = inferAccountType(account)) {\n this.error = '';\n this.connecting = true;\n try {\n const result = await _services_imClient__WEBPACK_IMPORTED_MODULE_2__.imClient.loginWithPassword(account, password, type);\n this.applyLoginResult(result);\n await this.connect();\n } catch (error) {\n this.error = error.message || '登录失败';\n } finally {\n this.connecting = false;\n }\n },\n async loginWithCode(account, code) {\n this.error = '';\n this.connecting = true;\n try {\n const result = await _services_imClient__WEBPACK_IMPORTED_MODULE_2__.imClient.loginWithCode(account, code);\n this.applyLoginResult(result);\n await this.connect();\n } catch (error) {\n this.error = error.message || '登录失败';\n } finally {\n this.connecting = false;\n }\n },\n async completePcSessionLogin(result) {\n this.error = '';\n this.connecting = true;\n try {\n this.applyLoginResult(result);\n await this.connect();\n } catch (error) {\n this.error = error.message || '扫码登录失败';\n } finally {\n this.connecting = false;\n }\n },\n async register(payload) {\n this.error = '';\n this.connecting = true;\n try {\n const result = await _services_imClient__WEBPACK_IMPORTED_MODULE_2__.imClient.register(payload);\n this.applyLoginResult(result);\n await this.connect();\n return result;\n } catch (error) {\n this.error = error.message || '注册失败';\n return null;\n } finally {\n this.connecting = false;\n }\n },\n applyLoginResult(result) {\n this.userId = result.userId || result.uid || result.id || '';\n this.token = result.token || result.imToken || '';\n this.displayName = result.displayName || result.userName || result.name || this.userId;\n this.portrait = normalizeStoredPortrait(result.portrait);\n this.isAuthenticated = Boolean(this.userId && this.token);\n this.persistSession();\n },\n persistSession() {\n localStorage.setItem(STORAGE_KEY, JSON.stringify({\n userId: this.userId,\n token: this.token,\n displayName: this.displayName,\n portrait: this.portrait,\n clientId: _services_imClient__WEBPACK_IMPORTED_MODULE_2__.imClient.getClientId(),\n appServer: _config__WEBPACK_IMPORTED_MODULE_3__.appConfig.appServer\n }));\n },\n applyProfile(profile) {\n if (!profile) {\n return;\n }\n this.userId = profile.userId || profile.uid || this.userId;\n this.displayName = profile.displayName || profile.name || this.displayName || this.userId;\n this.portrait = nextProfilePortrait(profile.portrait, this.portrait);\n this.persistSession();\n },\n async refreshProfile() {\n if (!this.userId) {\n return null;\n }\n try {\n const profile = await _services_imClient__WEBPACK_IMPORTED_MODULE_2__.imClient.getProfile(this.userId);\n this.applyProfile(profile);\n return profile;\n } catch (error) {\n return null;\n }\n },\n async saveProfile({\n displayName,\n portraitFile\n }) {\n if (!this.userId) {\n throw new Error('请先登录');\n }\n const profile = await _services_imClient__WEBPACK_IMPORTED_MODULE_2__.imClient.updateProfile(this.userId, {\n displayName,\n portraitFile\n });\n this.applyProfile(profile);\n return profile;\n },\n async changePassword({\n oldPassword,\n newPassword\n }) {\n if (!this.userId) {\n throw new Error('请先登录');\n }\n await _services_imClient__WEBPACK_IMPORTED_MODULE_2__.imClient.changePassword({\n oldPassword,\n newPassword\n });\n await this.logout();\n return true;\n },\n async resetPasswordWithCode({\n mobile,\n resetCode,\n newPassword\n }) {\n await _services_imClient__WEBPACK_IMPORTED_MODULE_2__.imClient.resetPasswordWithCode({\n mobile,\n resetCode,\n newPassword\n });\n return true;\n },\n async connect() {\n if (!this.userId || !this.token) {\n return;\n }\n await _services_imClient__WEBPACK_IMPORTED_MODULE_2__.imClient.ensureSdk();\n this.sdkVersion = _services_imClient__WEBPACK_IMPORTED_MODULE_2__.imClient.getSdkVersion();\n this.bindImEvents();\n await _services_imClient__WEBPACK_IMPORTED_MODULE_2__.imClient.connect(this.userId, this.token);\n },\n bindImEvents() {\n if (this.eventsBound) {\n return;\n }\n this.eventsBound = true;\n _services_imClient__WEBPACK_IMPORTED_MODULE_2__.imClient.on('connectionStatusChanged', status => {\n this.connected = status.connected;\n this.connectionStatusText = status.statusText || (status.connected ? '已连接' : '未连接');\n });\n },\n async logout() {\n try {\n await _services_imClient__WEBPACK_IMPORTED_MODULE_2__.imClient.logout?.();\n } catch (error) {\n this.error = error.message || '退出登录失败';\n } finally {\n _services_imClient__WEBPACK_IMPORTED_MODULE_2__.imClient.disconnect();\n localStorage.removeItem(STORAGE_KEY);\n this.$reset();\n }\n }\n }\n});\n\n//# sourceURL=webpack://jc-web-im/./src/stores/session.js?\n}"); + +/***/ }), + +/***/ "./node_modules/css-loader/dist/cjs.js??clonedRuleSet-14.use[1]!./node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-14.use[2]!./src/styles/base.css": +/*!***************************************************************************************************************************************************************!*\ + !*** ./node_modules/css-loader/dist/cjs.js??clonedRuleSet-14.use[1]!./node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-14.use[2]!./src/styles/base.css ***! + \***************************************************************************************************************************************************************/ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/noSourceMaps.js */ \"./node_modules/css-loader/dist/runtime/noSourceMaps.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0__);\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../../node_modules/css-loader/dist/runtime/api.js */ \"./node_modules/css-loader/dist/runtime/api.js\");\n/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1__);\n// Imports\n\n\nvar ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_1___default()((_node_modules_css_loader_dist_runtime_noSourceMaps_js__WEBPACK_IMPORTED_MODULE_0___default()));\n// Module\n___CSS_LOADER_EXPORT___.push([module.id, \":root {\\r\\n color-scheme: light;\\r\\n --bg: #f4f6f8;\\r\\n --panel: #ffffff;\\r\\n --panel-soft: #f8fafc;\\r\\n --line: #d9e0e7;\\r\\n --text: #18212f;\\r\\n --muted: #6a7584;\\r\\n --primary: #177d72;\\r\\n --primary-strong: #0f625a;\\r\\n --accent: #d84e3f;\\r\\n --bubble-in: #ffffff;\\r\\n --bubble-out: #dff4ef;\\r\\n --rail: #202a33;\\r\\n}\\r\\n\\r\\n* {\\r\\n box-sizing: border-box;\\r\\n}\\r\\n\\r\\nhtml,\\r\\nbody,\\r\\n#app {\\r\\n width: 100%;\\r\\n height: 100%;\\r\\n overflow: hidden;\\r\\n}\\r\\n\\r\\nbody {\\r\\n margin: 0;\\r\\n min-width: 320px;\\r\\n background: var(--bg);\\r\\n color: var(--text);\\r\\n font-family: -apple-system, BlinkMacSystemFont, \\\"Segoe UI\\\", sans-serif;\\r\\n}\\r\\n\\r\\nbutton,\\r\\ninput,\\r\\ntextarea {\\r\\n font: inherit;\\r\\n}\\r\\n\\r\\nbutton {\\r\\n cursor: pointer;\\r\\n}\\r\\n\\r\\n.login-page {\\r\\n min-height: 100vh;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n padding: 24px;\\r\\n background: linear-gradient(135deg, #eef6f3 0%, #f7f3ee 55%, #eef2f7 100%);\\r\\n}\\r\\n\\r\\n.login-panel {\\r\\n width: min(420px, 100%);\\r\\n padding: 32px;\\r\\n background: var(--panel);\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 8px;\\r\\n box-shadow: 0 18px 48px rgba(30, 40, 50, 0.12);\\r\\n}\\r\\n\\r\\n.brand-lockup {\\r\\n display: flex;\\r\\n align-items: center;\\r\\n gap: 14px;\\r\\n margin-bottom: 28px;\\r\\n}\\r\\n\\r\\n.brand-mark,\\r\\n.rail-avatar,\\r\\n.conversation-avatar {\\r\\n display: grid;\\r\\n place-items: center;\\r\\n flex: 0 0 auto;\\r\\n}\\r\\n\\r\\n.image-avatar {\\r\\n -o-object-fit: cover;\\r\\n object-fit: cover;\\r\\n}\\r\\n\\r\\n.brand-mark {\\r\\n width: 48px;\\r\\n height: 48px;\\r\\n border-radius: 8px;\\r\\n -o-object-fit: contain;\\r\\n object-fit: contain;\\r\\n background: #fff;\\r\\n}\\r\\n\\r\\n.brand-lockup h1,\\r\\n.list-header h2,\\r\\n.chat-header h2 {\\r\\n margin: 0;\\r\\n font-size: 20px;\\r\\n}\\r\\n\\r\\n.brand-lockup p,\\r\\n.list-header p,\\r\\n.chat-header p {\\r\\n margin: 4px 0 0;\\r\\n color: var(--muted);\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.login-form {\\r\\n display: grid;\\r\\n gap: 16px;\\r\\n}\\r\\n\\r\\n.login-tabs,\\r\\n.code-row,\\r\\n.phone-row,\\r\\n.captcha-row {\\r\\n display: grid;\\r\\n grid-template-columns: 1fr 1fr;\\r\\n gap: 8px;\\r\\n}\\r\\n\\r\\n.login-tabs button,\\r\\n.code-action {\\r\\n height: 38px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n background: #fff;\\r\\n color: var(--text);\\r\\n}\\r\\n\\r\\n.login-tabs button.active {\\r\\n border-color: var(--primary);\\r\\n background: #e8f4f2;\\r\\n color: var(--primary-strong);\\r\\n font-weight: 700;\\r\\n}\\r\\n\\r\\n.login-tabs {\\r\\n grid-template-columns: repeat(3, minmax(0, 1fr));\\r\\n}\\r\\n\\r\\n.login-tabs button {\\r\\n min-width: 0;\\r\\n padding: 0 6px;\\r\\n overflow-wrap: anywhere;\\r\\n}\\r\\n\\r\\n.code-row {\\r\\n grid-template-columns: minmax(0, 1fr) 112px;\\r\\n}\\r\\n\\r\\n.captcha-row {\\r\\n grid-template-columns: minmax(0, 1fr) 112px;\\r\\n}\\r\\n\\r\\n.phone-row {\\r\\n grid-template-columns: 86px minmax(0, 1fr);\\r\\n}\\r\\n\\r\\n.country-code-field {\\r\\n height: 42px;\\r\\n display: grid;\\r\\n grid-template-columns: 24px minmax(0, 1fr);\\r\\n align-items: center;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n background: #fff;\\r\\n color: var(--text);\\r\\n overflow: hidden;\\r\\n}\\r\\n\\r\\n.country-code-field:focus-within {\\r\\n border-color: var(--primary);\\r\\n box-shadow: 0 0 0 3px rgba(23, 125, 114, 0.12);\\r\\n}\\r\\n\\r\\n.country-code-field span {\\r\\n display: grid;\\r\\n place-items: center;\\r\\n color: var(--muted);\\r\\n font-weight: 700;\\r\\n}\\r\\n\\r\\n.code-action:disabled {\\r\\n cursor: not-allowed;\\r\\n opacity: 0.55;\\r\\n}\\r\\n\\r\\n.login-form label {\\r\\n display: grid;\\r\\n gap: 8px;\\r\\n color: var(--muted);\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.label-row {\\r\\n min-height: 18px;\\r\\n display: grid;\\r\\n grid-template-columns: 86px minmax(0, 1fr);\\r\\n align-items: center;\\r\\n gap: 8px;\\r\\n}\\r\\n\\r\\n.field-error-badge {\\r\\n min-height: 28px;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n padding: 4px 10px;\\r\\n border-radius: 2px;\\r\\n background: var(--accent);\\r\\n color: #fff;\\r\\n font-weight: 700;\\r\\n line-height: 1.2;\\r\\n text-align: center;\\r\\n}\\r\\n\\r\\n.login-form input,\\r\\n.login-form select,\\r\\n.modal-panel input,\\r\\n.modal-panel textarea,\\r\\n.search-box input,\\r\\n.message-composer textarea {\\r\\n width: 100%;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n outline: none;\\r\\n background: #fff;\\r\\n color: var(--text);\\r\\n}\\r\\n\\r\\n.login-form input,\\r\\n.login-form select,\\r\\n.modal-panel input,\\r\\n.search-box input {\\r\\n height: 42px;\\r\\n padding: 0 12px;\\r\\n}\\r\\n\\r\\n.modal-panel textarea {\\r\\n min-height: 92px;\\r\\n resize: vertical;\\r\\n padding: 10px 12px;\\r\\n line-height: 1.5;\\r\\n}\\r\\n\\r\\n.login-form .country-code-field input {\\r\\n height: 40px;\\r\\n padding: 0 10px 0 0;\\r\\n border: 0;\\r\\n border-radius: 0;\\r\\n}\\r\\n\\r\\n.login-form .country-code-field input:focus {\\r\\n border-color: transparent;\\r\\n box-shadow: none;\\r\\n}\\r\\n\\r\\n.login-form input:focus,\\r\\n.login-form select:focus,\\r\\n.modal-panel input:focus,\\r\\n.modal-panel textarea:focus,\\r\\n.search-box input:focus,\\r\\n.message-composer textarea:focus {\\r\\n border-color: var(--primary);\\r\\n box-shadow: 0 0 0 3px rgba(23, 125, 114, 0.12);\\r\\n}\\r\\n\\r\\n.login-form input.captcha-input-valid {\\r\\n border-color: var(--primary);\\r\\n background: var(--primary);\\r\\n color: #fff;\\r\\n font-weight: 700;\\r\\n}\\r\\n\\r\\n.login-form input.captcha-input-valid::-moz-placeholder {\\r\\n color: rgba(255, 255, 255, 0.75);\\r\\n}\\r\\n\\r\\n.login-form input.captcha-input-valid::placeholder {\\r\\n color: rgba(255, 255, 255, 0.75);\\r\\n}\\r\\n\\r\\n.login-form input.captcha-input-invalid {\\r\\n border-color: var(--accent);\\r\\n color: var(--accent);\\r\\n box-shadow: 0 0 0 3px rgba(216, 78, 63, 0.12);\\r\\n}\\r\\n\\r\\n.login-form input.field-input-error {\\r\\n border-color: var(--accent);\\r\\n color: var(--accent);\\r\\n box-shadow: 0 0 0 3px rgba(216, 78, 63, 0.12);\\r\\n}\\r\\n\\r\\n.login-form input.field-input-error::-moz-placeholder {\\r\\n color: rgba(216, 78, 63, 0.78);\\r\\n}\\r\\n\\r\\n.login-form input.field-input-error::placeholder {\\r\\n color: rgba(216, 78, 63, 0.78);\\r\\n}\\r\\n\\r\\n.captcha-button {\\r\\n width: 112px;\\r\\n height: 42px;\\r\\n padding: 0;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n background: #f7fbfa;\\r\\n overflow: hidden;\\r\\n}\\r\\n\\r\\n.captcha-button img {\\r\\n display: block;\\r\\n width: 100%;\\r\\n height: 100%;\\r\\n -o-object-fit: contain;\\r\\n object-fit: contain;\\r\\n}\\r\\n\\r\\n.qr-login-card {\\r\\n position: relative;\\r\\n width: min(248px, 100%);\\r\\n aspect-ratio: 1;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n justify-self: center;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 8px;\\r\\n background: #fff;\\r\\n overflow: hidden;\\r\\n}\\r\\n\\r\\n.qr-code-button {\\r\\n width: 100%;\\r\\n height: 100%;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n padding: 18px;\\r\\n border: 0;\\r\\n background: #fff;\\r\\n color: var(--muted);\\r\\n font-weight: 700;\\r\\n}\\r\\n\\r\\n.qr-code-button:disabled {\\r\\n cursor: default;\\r\\n}\\r\\n\\r\\n.qr-code-button img {\\r\\n width: 100%;\\r\\n height: 100%;\\r\\n -o-object-fit: contain;\\r\\n object-fit: contain;\\r\\n image-rendering: pixelated;\\r\\n}\\r\\n\\r\\n.qr-login-card.scanned .qr-code-button,\\r\\n.qr-login-card.expired .qr-code-button,\\r\\n.qr-login-card.error .qr-code-button {\\r\\n opacity: 0.22;\\r\\n}\\r\\n\\r\\n.qr-scan-user {\\r\\n position: absolute;\\r\\n inset: 0;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n background: rgba(255, 255, 255, 0.76);\\r\\n}\\r\\n\\r\\n.qr-scan-user img,\\r\\n.qr-scan-user span {\\r\\n width: 70px;\\r\\n height: 70px;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n border-radius: 8px;\\r\\n background: var(--primary);\\r\\n color: #fff;\\r\\n font-size: 28px;\\r\\n font-weight: 800;\\r\\n -o-object-fit: cover;\\r\\n object-fit: cover;\\r\\n}\\r\\n\\r\\n.qr-login-status {\\r\\n min-height: 48px;\\r\\n display: grid;\\r\\n gap: 4px;\\r\\n text-align: center;\\r\\n}\\r\\n\\r\\n.qr-login-status strong {\\r\\n color: var(--text);\\r\\n font-size: 15px;\\r\\n}\\r\\n\\r\\n.qr-login-status span {\\r\\n color: var(--muted);\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.primary-action,\\r\\n.primary-inline-action,\\r\\n.send-button {\\r\\n border: 0;\\r\\n border-radius: 6px;\\r\\n background: var(--primary);\\r\\n color: #fff;\\r\\n font-weight: 700;\\r\\n}\\r\\n\\r\\n.primary-action {\\r\\n height: 44px;\\r\\n}\\r\\n\\r\\n.primary-action:hover,\\r\\n.primary-inline-action:hover,\\r\\n.send-button:hover {\\r\\n background: var(--primary-strong);\\r\\n}\\r\\n\\r\\n.primary-action:disabled,\\r\\n.primary-inline-action:disabled,\\r\\n.send-button:disabled {\\r\\n cursor: not-allowed;\\r\\n opacity: 0.55;\\r\\n}\\r\\n\\r\\n.ghost-action {\\r\\n width: 100%;\\r\\n height: 40px;\\r\\n margin-top: 12px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n background: #fff;\\r\\n color: var(--text);\\r\\n}\\r\\n\\r\\n.ghost-inline-action,\\r\\n.primary-inline-action,\\r\\n.danger-inline-action {\\r\\n height: 38px;\\r\\n display: inline-flex;\\r\\n align-items: center;\\r\\n justify-content: center;\\r\\n padding: 0 16px;\\r\\n border-radius: 6px;\\r\\n font-weight: 700;\\r\\n line-height: 1;\\r\\n}\\r\\n\\r\\n.ghost-inline-action {\\r\\n border: 1px solid var(--line);\\r\\n background: #fff;\\r\\n color: var(--text);\\r\\n}\\r\\n\\r\\n.ghost-inline-action:hover {\\r\\n background: #edf2f5;\\r\\n}\\r\\n\\r\\n.danger-inline-action {\\r\\n border: 1px solid #ef4444;\\r\\n background: #fff;\\r\\n color: #b91c1c;\\r\\n}\\r\\n\\r\\n.danger-inline-action:hover {\\r\\n background: #fee2e2;\\r\\n}\\r\\n\\r\\n.form-error {\\r\\n margin: 0;\\r\\n color: var(--accent);\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.form-info {\\r\\n margin: 0;\\r\\n color: var(--muted);\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.app-shell {\\n height: 100dvh;\\n min-height: 0;\\n display: grid;\\n grid-template-columns: 64px minmax(260px, 340px) minmax(0, 1fr);\\n overflow: hidden;\\n background: var(--panel);\\n}\\n\\n.documented-shell {\\n min-height: 100dvh;\\n display: grid;\\n grid-template-columns: 260px minmax(0, 1fr);\\n background:\\n radial-gradient(circle at top left, rgba(23, 125, 114, 0.08), transparent 28%),\\n linear-gradient(180deg, #f4f8f7 0%, #eef3f7 100%);\\n}\\n\\n.documented-rail {\\n min-height: 0;\\n display: grid;\\n align-content: start;\\n gap: 14px;\\n padding: 24px 20px;\\n border-right: 1px solid var(--line);\\n background: rgba(255, 255, 255, 0.82);\\n backdrop-filter: blur(12px);\\n}\\n\\n.documented-avatar {\\n width: 64px;\\n height: 64px;\\n display: grid;\\n place-items: center;\\n overflow: hidden;\\n border-radius: 16px;\\n background: #dff4ef;\\n color: var(--primary-strong);\\n font-size: 28px;\\n font-weight: 800;\\n}\\n\\n.documented-avatar img {\\n width: 100%;\\n height: 100%;\\n -o-object-fit: cover;\\n object-fit: cover;\\n}\\n\\n.documented-rail-copy {\\n display: grid;\\n gap: 4px;\\n}\\n\\n.documented-rail-copy strong {\\n font-size: 16px;\\n}\\n\\n.documented-rail-copy small {\\n color: var(--muted);\\n overflow-wrap: anywhere;\\n}\\n\\n.documented-rail-button {\\n min-height: 40px;\\n padding: 0 14px;\\n border: 0;\\n border-radius: 10px;\\n background: var(--primary);\\n color: #fff;\\n font-weight: 700;\\n text-align: left;\\n}\\n\\n.documented-rail-button.ghost {\\n border: 1px solid var(--line);\\n background: #fff;\\n color: var(--text);\\n}\\n\\n.documented-main {\\n min-width: 0;\\n min-height: 0;\\n overflow: auto;\\n padding: 28px;\\n display: grid;\\n align-content: start;\\n gap: 18px;\\n}\\n\\n.documented-header {\\n display: flex;\\n align-items: end;\\n justify-content: space-between;\\n gap: 18px;\\n}\\n\\n.documented-header h1 {\\n margin: 0;\\n font-size: 28px;\\n}\\n\\n.documented-header p {\\n margin: 6px 0 0;\\n color: var(--muted);\\n}\\n\\n.documented-status {\\n min-width: 180px;\\n display: grid;\\n gap: 4px;\\n padding: 14px 16px;\\n border: 1px solid var(--line);\\n border-radius: 14px;\\n background: rgba(255, 255, 255, 0.88);\\n}\\n\\n.documented-status span {\\n color: var(--muted);\\n font-size: 12px;\\n}\\n\\n.documented-status strong {\\n font-size: 15px;\\n}\\n\\n.documented-card {\\n display: grid;\\n gap: 12px;\\n padding: 20px;\\n border: 1px solid var(--line);\\n border-radius: 16px;\\n background: rgba(255, 255, 255, 0.88);\\n box-shadow: 0 16px 36px rgba(24, 33, 47, 0.06);\\n}\\n\\n.documented-card h2 {\\n margin: 0;\\n font-size: 18px;\\n}\\n\\n.documented-text {\\n margin: 0;\\n color: var(--muted);\\n line-height: 1.6;\\n}\\n\\n.documented-list {\\n margin: 0;\\n padding-left: 18px;\\n display: grid;\\n gap: 8px;\\n line-height: 1.55;\\n}\\n\\n.documented-list.muted {\\n color: var(--muted);\\n}\\n\\n.documented-meta {\\n margin: 0;\\n display: grid;\\n gap: 10px;\\n}\\n\\n.documented-meta div {\\n display: grid;\\n grid-template-columns: 110px minmax(0, 1fr);\\n gap: 12px;\\n padding-top: 10px;\\n border-top: 1px solid var(--line);\\n}\\n\\n.documented-meta dt {\\n color: var(--muted);\\n}\\n\\n.documented-meta dd {\\n margin: 0;\\n overflow-wrap: anywhere;\\n}\\n\\n.documented-password-panel {\\n width: min(100%, 360px);\\n}\\n\\r\\n.navigation-rail {\\r\\n min-height: 0;\\r\\n display: flex;\\r\\n flex-direction: column;\\r\\n align-items: center;\\r\\n gap: 14px;\\r\\n padding: 16px 10px;\\r\\n background: var(--rail);\\r\\n}\\r\\n\\r\\n.rail-avatar {\\r\\n width: 40px;\\r\\n height: 40px;\\r\\n border-radius: 8px;\\r\\n background: #e9f7f4;\\r\\n color: var(--primary-strong);\\r\\n font-weight: 800;\\r\\n overflow: hidden;\\r\\n}\\r\\n\\r\\n.rail-avatar-button {\\r\\n position: relative;\\r\\n border: 0;\\r\\n padding: 0;\\r\\n cursor: pointer;\\r\\n}\\r\\n\\r\\n.rail-avatar-button::after {\\r\\n content: \\\"+\\\";\\r\\n position: absolute;\\r\\n right: -2px;\\r\\n bottom: -2px;\\r\\n width: 15px;\\r\\n height: 15px;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n border: 1px solid var(--rail);\\r\\n border-radius: 50%;\\r\\n background: var(--primary);\\r\\n color: #fff;\\r\\n font-size: 11px;\\r\\n font-weight: 800;\\r\\n line-height: 1;\\r\\n}\\r\\n\\r\\n.rail-avatar-button:hover,\\r\\n.rail-avatar-button:focus-visible {\\r\\n box-shadow: 0 0 0 3px rgba(47, 196, 178, 0.28);\\r\\n}\\r\\n\\r\\n.rail-avatar img {\\r\\n width: 100%;\\r\\n height: 100%;\\r\\n -o-object-fit: cover;\\r\\n object-fit: cover;\\r\\n}\\r\\n\\r\\n.rail-button,\\r\\n.icon-button {\\r\\n display: grid;\\r\\n place-items: center;\\r\\n border: 0;\\r\\n border-radius: 6px;\\r\\n background: transparent;\\r\\n}\\r\\n\\r\\n.rail-button {\\r\\n width: 40px;\\r\\n height: 40px;\\r\\n color: #dbe5ea;\\r\\n font-size: 18px;\\r\\n}\\r\\n\\r\\n.rail-button svg {\\r\\n width: 22px;\\r\\n height: 22px;\\r\\n fill: none;\\r\\n stroke: currentColor;\\r\\n stroke-width: 1.8;\\r\\n stroke-linecap: round;\\r\\n stroke-linejoin: round;\\r\\n}\\r\\n\\r\\n.rail-button.active,\\r\\n.rail-button:hover {\\r\\n background: rgba(255, 255, 255, 0.12);\\r\\n}\\r\\n\\r\\n.rail-logout-button:hover {\\r\\n background: rgba(239, 68, 68, 0.2);\\r\\n color: #fecaca;\\r\\n}\\r\\n\\r\\n.rail-spacer {\\r\\n flex: 1;\\r\\n}\\r\\n\\r\\n.conversation-list {\\r\\n position: relative;\\r\\n min-height: 0;\\r\\n display: grid;\\r\\n grid-template-rows: 72px auto minmax(0, 1fr);\\r\\n border-right: 1px solid var(--line);\\r\\n background: var(--panel-soft);\\r\\n overflow: hidden;\\r\\n}\\r\\n\\r\\n.modal-backdrop {\\r\\n position: absolute;\\r\\n inset: 0;\\r\\n z-index: 5;\\r\\n display: grid;\\r\\n place-items: start center;\\r\\n padding: 88px 16px 16px;\\r\\n background: rgba(24, 33, 47, 0.28);\\r\\n}\\r\\n\\r\\n.modal-panel {\\r\\n width: min(100%, 308px);\\r\\n display: grid;\\r\\n gap: 14px;\\r\\n padding: 16px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 8px;\\r\\n background: var(--panel);\\r\\n box-shadow: 0 18px 40px rgba(24, 33, 47, 0.18);\\r\\n}\\r\\n\\r\\n.modal-panel header,\\r\\n.modal-actions {\\r\\n display: flex;\\r\\n align-items: center;\\r\\n justify-content: space-between;\\r\\n gap: 10px;\\r\\n}\\r\\n\\r\\n.modal-panel header strong {\\r\\n font-size: 16px;\\r\\n}\\r\\n\\r\\n.modal-panel label {\\r\\n display: grid;\\r\\n gap: 8px;\\r\\n color: var(--muted);\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.settings-backdrop {\\r\\n z-index: 24;\\r\\n place-items: center;\\r\\n padding: 24px 16px;\\r\\n}\\r\\n\\r\\n.settings-panel {\\r\\n width: min(620px, calc(100vw - 32px));\\r\\n max-height: calc(100dvh - 48px);\\r\\n display: grid;\\r\\n gap: 14px;\\r\\n overflow: auto;\\r\\n padding: 16px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 8px;\\r\\n background: var(--panel);\\r\\n box-shadow: 0 18px 40px rgba(24, 33, 47, 0.18);\\r\\n}\\r\\n\\r\\n.settings-panel header,\\r\\n.settings-password-form footer {\\r\\n display: flex;\\r\\n align-items: center;\\r\\n justify-content: space-between;\\r\\n gap: 12px;\\r\\n}\\r\\n\\r\\n.settings-panel header strong {\\r\\n font-size: 16px;\\r\\n}\\r\\n\\r\\n.settings-layout {\\r\\n display: grid;\\r\\n grid-template-columns: 170px minmax(0, 1fr);\\r\\n gap: 14px;\\r\\n}\\r\\n\\r\\n.settings-menu {\\r\\n display: grid;\\r\\n align-content: start;\\r\\n gap: 8px;\\r\\n}\\r\\n\\r\\n.settings-menu button {\\r\\n min-height: 46px;\\r\\n display: grid;\\r\\n gap: 2px;\\r\\n justify-items: start;\\r\\n padding: 8px 10px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n background: #fff;\\r\\n color: var(--text);\\r\\n text-align: left;\\r\\n}\\r\\n\\r\\n.settings-menu button.active,\\r\\n.settings-menu button:hover {\\r\\n border-color: var(--primary);\\r\\n background: #e8f4f2;\\r\\n color: var(--primary-strong);\\r\\n}\\r\\n\\r\\n.settings-menu button span {\\r\\n font-weight: 700;\\r\\n}\\r\\n\\r\\n.settings-menu button small {\\r\\n color: var(--muted);\\r\\n font-size: 11px;\\r\\n}\\r\\n\\r\\n.settings-password-form,\\r\\n.settings-placeholder {\\r\\n min-height: 250px;\\r\\n display: grid;\\r\\n align-content: start;\\r\\n gap: 12px;\\r\\n}\\r\\n\\r\\n.settings-password-form h3,\\r\\n.settings-placeholder h3 {\\r\\n margin: 0;\\r\\n font-size: 16px;\\r\\n}\\r\\n\\r\\n.settings-password-form label {\\r\\n display: grid;\\r\\n gap: 8px;\\r\\n color: var(--muted);\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.settings-password-form input {\\r\\n width: 100%;\\r\\n height: 42px;\\r\\n padding: 0 12px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n outline: none;\\r\\n background: #fff;\\r\\n color: var(--text);\\r\\n}\\r\\n\\r\\n.settings-password-form input:focus {\\r\\n border-color: var(--primary);\\r\\n box-shadow: 0 0 0 3px rgba(23, 125, 114, 0.12);\\r\\n}\\r\\n\\r\\n.settings-password-form footer {\\r\\n margin-top: 4px;\\r\\n justify-content: flex-end;\\r\\n flex-wrap: wrap;\\r\\n}\\r\\n\\r\\n.settings-password-form footer .inline-save-status {\\r\\n margin-right: auto;\\r\\n}\\r\\n\\r\\n.settings-placeholder {\\r\\n place-items: center;\\r\\n border: 1px dashed var(--line);\\r\\n border-radius: 8px;\\r\\n color: var(--muted);\\r\\n text-align: center;\\r\\n}\\r\\n\\r\\n.settings-placeholder p {\\r\\n margin: 0;\\r\\n}\\r\\n\\r\\n.profile-edit-panel {\\r\\n width: min(100%, 360px);\\r\\n}\\r\\n\\r\\n.profile-avatar-field {\\r\\n display: grid;\\r\\n justify-items: center;\\r\\n gap: 10px;\\r\\n}\\r\\n\\r\\n.profile-avatar-field input[type=\\\"file\\\"] {\\r\\n position: absolute;\\r\\n inline-size: 1px;\\r\\n block-size: 1px;\\r\\n opacity: 0;\\r\\n pointer-events: none;\\r\\n}\\r\\n\\r\\n.profile-avatar-preview {\\r\\n width: 88px;\\r\\n height: 88px;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n overflow: hidden;\\r\\n padding: 0;\\r\\n border: 0;\\r\\n border-radius: 8px;\\r\\n background: #dce8f2;\\r\\n color: #24435c;\\r\\n cursor: default;\\r\\n font-size: 30px;\\r\\n font-weight: 800;\\r\\n}\\r\\n\\r\\n.profile-avatar-preview.clickable {\\r\\n cursor: zoom-in;\\r\\n}\\r\\n\\r\\n.profile-avatar-preview.clickable:hover {\\r\\n box-shadow: 0 0 0 3px rgba(23, 125, 114, 0.16);\\r\\n}\\r\\n\\r\\n.profile-avatar-preview img {\\r\\n width: 100%;\\r\\n height: 100%;\\r\\n -o-object-fit: cover;\\r\\n object-fit: cover;\\r\\n}\\r\\n\\r\\n.profile-avatar-change-button {\\r\\n min-width: 96px;\\r\\n}\\r\\n\\r\\n.avatar-preview-backdrop {\\r\\n z-index: 32;\\r\\n place-items: center;\\r\\n padding: 24px 16px;\\r\\n}\\r\\n\\r\\n.avatar-preview-dialog {\\r\\n width: min(520px, calc(100vw - 32px));\\r\\n max-height: calc(100dvh - 48px);\\r\\n display: grid;\\r\\n gap: 12px;\\r\\n padding: 14px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 8px;\\r\\n background: var(--panel);\\r\\n box-shadow: 0 18px 40px rgba(24, 33, 47, 0.18);\\r\\n}\\r\\n\\r\\n.avatar-preview-dialog header {\\r\\n display: flex;\\r\\n align-items: center;\\r\\n justify-content: space-between;\\r\\n gap: 12px;\\r\\n}\\r\\n\\r\\n.avatar-preview-actions {\\r\\n display: flex;\\r\\n align-items: center;\\r\\n gap: 6px;\\r\\n}\\r\\n\\r\\n.avatar-preview-actions button {\\r\\n min-width: 34px;\\r\\n height: 34px;\\r\\n display: inline-flex;\\r\\n align-items: center;\\r\\n justify-content: center;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n background: #fff;\\r\\n color: var(--text);\\r\\n font-weight: 700;\\r\\n}\\r\\n\\r\\n.avatar-preview-actions .icon-button {\\r\\n border: 0;\\r\\n color: var(--muted);\\r\\n}\\r\\n\\r\\n.avatar-preview-stage {\\r\\n height: min(430px, calc(100dvh - 160px));\\r\\n min-height: 260px;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n overflow: hidden;\\r\\n border-radius: 8px;\\r\\n background: #101820;\\r\\n cursor: zoom-in;\\r\\n touch-action: none;\\r\\n}\\r\\n\\r\\n.avatar-preview-stage.dragging {\\r\\n cursor: grabbing;\\r\\n}\\r\\n\\r\\n.avatar-preview-stage img {\\r\\n max-width: min(100%, 430px);\\r\\n max-height: 100%;\\r\\n border-radius: 8px;\\r\\n -o-object-fit: contain;\\r\\n object-fit: contain;\\r\\n transform-origin: center center;\\r\\n transition: transform 0.08s ease-out;\\r\\n -webkit-user-select: none;\\r\\n -moz-user-select: none;\\r\\n user-select: none;\\r\\n pointer-events: none;\\r\\n}\\r\\n\\r\\n.avatar-preview-stage.dragging img {\\r\\n transition: none;\\r\\n}\\r\\n\\r\\n.avatar-cropper-backdrop {\\r\\n z-index: 30;\\r\\n place-items: center;\\r\\n padding: 24px 16px;\\r\\n}\\r\\n\\r\\n.avatar-cropper-panel {\\r\\n width: min(720px, calc(100vw - 32px));\\r\\n max-height: calc(100dvh - 48px);\\r\\n overflow: auto;\\r\\n}\\r\\n\\r\\n.avatar-cropper-header-actions {\\r\\n display: flex;\\r\\n align-items: center;\\r\\n gap: 8px;\\r\\n}\\r\\n\\r\\n.avatar-cropper-layout {\\r\\n display: grid;\\r\\n grid-template-columns: minmax(0, 1fr) 190px;\\r\\n gap: 14px;\\r\\n}\\r\\n\\r\\n.avatar-cropper-stage {\\r\\n position: relative;\\r\\n height: 360px;\\r\\n min-height: 360px;\\r\\n overflow: hidden;\\r\\n border: 1px dashed #b8ccd7;\\r\\n border-radius: 8px;\\r\\n background: #0f1b24;\\r\\n touch-action: none;\\r\\n cursor: grab;\\r\\n}\\r\\n\\r\\n.avatar-cropper-stage.dragging {\\r\\n cursor: grabbing;\\r\\n}\\r\\n\\r\\n.avatar-cropper-stage canvas {\\r\\n position: absolute;\\r\\n inset: 0;\\r\\n width: 100%;\\r\\n height: 100%;\\r\\n display: block;\\r\\n}\\r\\n\\r\\n.avatar-cropper-box {\\r\\n position: absolute;\\r\\n left: 50%;\\r\\n top: 50%;\\r\\n width: 74%;\\r\\n aspect-ratio: 1;\\r\\n max-width: calc(100% - 44px);\\r\\n max-height: calc(100% - 44px);\\r\\n transform: translate(-50%, -50%);\\r\\n border: 2px dashed var(--primary);\\r\\n border-radius: 8px;\\r\\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.72);\\r\\n pointer-events: none;\\r\\n}\\r\\n\\r\\n.avatar-cropper-mask {\\r\\n position: absolute;\\r\\n inset: 0;\\r\\n pointer-events: none;\\r\\n}\\r\\n\\r\\n.avatar-cropper-placeholder {\\r\\n position: absolute;\\r\\n inset: 0;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n color: #d9e8ee;\\r\\n font-size: 13px;\\r\\n pointer-events: none;\\r\\n}\\r\\n\\r\\n.avatar-cropper-controls {\\r\\n min-width: 0;\\r\\n display: grid;\\r\\n align-content: start;\\r\\n gap: 12px;\\r\\n}\\r\\n\\r\\n.avatar-cropper-tools {\\r\\n display: grid;\\r\\n grid-template-columns: repeat(2, minmax(0, 1fr));\\r\\n gap: 8px;\\r\\n}\\r\\n\\r\\n.avatar-cropper-tools button {\\r\\n min-height: 34px;\\r\\n padding: 0 10px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n background: #fff;\\r\\n color: var(--text);\\r\\n font-weight: 700;\\r\\n}\\r\\n\\r\\n.avatar-cropper-tools button:hover {\\r\\n border-color: var(--primary);\\r\\n color: var(--primary-strong);\\r\\n}\\r\\n\\r\\n.avatar-cropper-preview {\\r\\n width: 108px;\\r\\n height: 108px;\\r\\n justify-self: center;\\r\\n overflow: hidden;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 8px;\\r\\n background: #eef4f7;\\r\\n}\\r\\n\\r\\n.avatar-cropper-preview canvas {\\r\\n width: 100%;\\r\\n height: 100%;\\r\\n display: block;\\r\\n}\\r\\n\\r\\n.avatar-cropper-controls footer {\\r\\n display: flex;\\r\\n align-items: center;\\r\\n justify-content: flex-end;\\r\\n flex-wrap: wrap;\\r\\n gap: 8px;\\r\\n}\\r\\n\\r\\n.avatar-cropper-controls footer .inline-save-status {\\r\\n flex: 1 1 100%;\\r\\n}\\r\\n\\r\\n.modal-actions {\\r\\n justify-content: flex-end;\\r\\n}\\r\\n\\r\\n.list-header,\\r\\n.chat-header {\\r\\n display: flex;\\r\\n align-items: center;\\r\\n justify-content: space-between;\\r\\n min-height: 72px;\\r\\n padding: 16px 18px;\\r\\n border-bottom: 1px solid var(--line);\\r\\n}\\r\\n\\r\\n.icon-button {\\r\\n width: 34px;\\r\\n height: 34px;\\r\\n color: var(--muted);\\r\\n font-size: 20px;\\r\\n}\\r\\n\\r\\n.icon-button:hover {\\r\\n background: #edf2f5;\\r\\n color: var(--text);\\r\\n}\\r\\n\\r\\n.search-box {\\r\\n padding: 12px 14px;\\r\\n}\\r\\n\\r\\n.sidebar-scroll {\\r\\n min-height: 0;\\r\\n overflow: auto;\\r\\n overscroll-behavior: contain;\\r\\n}\\r\\n\\r\\n.conversation-item {\\r\\n width: 100%;\\r\\n display: grid;\\r\\n grid-template-columns: 42px minmax(0, 1fr) auto;\\r\\n align-items: center;\\r\\n gap: 10px;\\r\\n padding: 12px 14px;\\r\\n border: 0;\\r\\n border-top: 1px solid rgba(217, 224, 231, 0.7);\\r\\n background: transparent;\\r\\n color: inherit;\\r\\n text-align: left;\\r\\n}\\r\\n\\r\\n.conversation-item.active,\\r\\n.conversation-item:hover {\\r\\n background: #eaf3f1;\\r\\n}\\r\\n\\r\\n.favorite-item {\\r\\n width: 100%;\\r\\n display: grid;\\r\\n grid-template-columns: 32px minmax(0, 1fr) auto;\\r\\n align-items: center;\\r\\n gap: 10px;\\r\\n padding: 12px 14px;\\r\\n border: 0;\\r\\n border-top: 1px solid rgba(217, 224, 231, 0.7);\\r\\n background: transparent;\\r\\n color: inherit;\\r\\n text-align: left;\\r\\n}\\r\\n\\r\\n.favorite-item.active,\\r\\n.favorite-item:hover {\\r\\n background: #eaf3f1;\\r\\n}\\r\\n\\r\\n.favorite-item-icon {\\r\\n width: 32px;\\r\\n height: 32px;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n border-radius: 6px;\\r\\n background: #fff7df;\\r\\n color: #c79520;\\r\\n font-size: 16px;\\r\\n}\\r\\n\\r\\n.favorite-item-main {\\r\\n min-width: 0;\\r\\n display: grid;\\r\\n gap: 4px;\\r\\n}\\r\\n\\r\\n.favorite-item-main strong,\\r\\n.favorite-item-main small {\\r\\n overflow: hidden;\\r\\n text-overflow: ellipsis;\\r\\n white-space: nowrap;\\r\\n}\\r\\n\\r\\n.favorite-item-main strong {\\r\\n font-size: 14px;\\r\\n}\\r\\n\\r\\n.favorite-item-main small,\\r\\n.favorite-item time {\\r\\n color: var(--muted);\\r\\n font-size: 12px;\\r\\n}\\r\\n\\r\\n.favorite-panel {\\r\\n min-width: 0;\\r\\n min-height: 0;\\r\\n display: grid;\\r\\n grid-template-rows: auto minmax(0, 1fr);\\r\\n background: #eef3f6;\\r\\n}\\r\\n\\r\\n.favorite-detail {\\r\\n min-height: 0;\\r\\n display: grid;\\r\\n align-content: start;\\r\\n gap: 16px;\\r\\n padding: 28px;\\r\\n overflow: auto;\\r\\n}\\r\\n\\r\\n.favorite-detail-message {\\r\\n width: min(680px, 100%);\\r\\n display: grid;\\r\\n grid-template-columns: auto minmax(0, 1fr);\\r\\n gap: 10px 12px;\\r\\n padding: 18px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 8px;\\r\\n background: var(--panel);\\r\\n box-shadow: 0 8px 24px rgba(24, 33, 47, 0.08);\\r\\n}\\r\\n\\r\\n.favorite-detail-message.recalled {\\r\\n border-color: #f1c7c1;\\r\\n background: #fff7f6;\\r\\n}\\r\\n\\r\\n.favorite-detail-star {\\r\\n color: #c79520;\\r\\n font-size: 18px;\\r\\n line-height: 1.4;\\r\\n}\\r\\n\\r\\n.favorite-detail-body {\\r\\n min-width: 0;\\r\\n display: grid;\\r\\n gap: 10px;\\r\\n}\\r\\n\\r\\n.favorite-detail-message p {\\r\\n margin: 0;\\r\\n white-space: pre-wrap;\\r\\n overflow-wrap: anywhere;\\r\\n line-height: 1.55;\\r\\n}\\r\\n\\r\\n.favorite-detail-message time {\\r\\n grid-column: 2;\\r\\n color: var(--muted);\\r\\n font-size: 12px;\\r\\n}\\r\\n\\r\\n.favorite-detail-image {\\r\\n width: min(420px, 100%);\\r\\n display: block;\\r\\n overflow: hidden;\\r\\n border: 1px solid #b9ded8;\\r\\n border-radius: 8px;\\r\\n background: #fff;\\r\\n}\\r\\n\\r\\n.favorite-detail-image img {\\r\\n display: block;\\r\\n width: 100%;\\r\\n max-height: 360px;\\r\\n -o-object-fit: contain;\\r\\n object-fit: contain;\\r\\n background: #f8fbfc;\\r\\n}\\r\\n\\r\\n.favorite-detail-file {\\r\\n width: min(360px, 100%);\\r\\n}\\r\\n\\r\\n.favorite-detail-actions {\\r\\n width: min(680px, 100%);\\r\\n display: flex;\\r\\n align-items: center;\\r\\n justify-content: flex-end;\\r\\n gap: 8px;\\r\\n}\\r\\n\\r\\n.favorite-detail-actions .inline-save-status {\\r\\n margin-right: auto;\\r\\n}\\r\\n\\r\\n.empty-list,\\r\\n.empty-chat {\\r\\n display: grid;\\r\\n place-items: center;\\r\\n align-content: center;\\r\\n gap: 8px;\\r\\n color: var(--muted);\\r\\n text-align: center;\\r\\n}\\r\\n\\r\\n.empty-list {\\r\\n min-height: 180px;\\r\\n padding: 24px;\\r\\n}\\r\\n\\r\\n.empty-chat {\\r\\n min-height: 0;\\r\\n padding: 24px;\\r\\n}\\r\\n\\r\\n.empty-list strong,\\r\\n.empty-chat strong {\\r\\n color: var(--text);\\r\\n}\\r\\n\\r\\n.conversation-avatar {\\r\\n width: 42px;\\r\\n height: 42px;\\r\\n border-radius: 8px;\\r\\n background: #dce8f2;\\r\\n color: #24435c;\\r\\n font-weight: 700;\\r\\n}\\r\\n\\r\\n.conversation-copy {\\r\\n min-width: 0;\\r\\n display: grid;\\r\\n gap: 4px;\\r\\n}\\r\\n\\r\\n.conversation-copy strong,\\r\\n.conversation-copy small {\\r\\n overflow: hidden;\\r\\n text-overflow: ellipsis;\\r\\n white-space: nowrap;\\r\\n}\\r\\n\\r\\n.conversation-copy small {\\r\\n color: var(--muted);\\r\\n}\\r\\n\\r\\n.unread-badge {\\r\\n justify-self: end;\\r\\n min-width: 20px;\\r\\n height: 20px;\\r\\n padding: 0 6px;\\r\\n border-radius: 999px;\\r\\n background: #ef4444;\\r\\n color: #fff;\\r\\n font-size: 12px;\\r\\n font-weight: 800;\\r\\n line-height: 20px;\\r\\n text-align: center;\\r\\n box-shadow: 0 0 0 2px #f7fafc;\\r\\n}\\r\\n\\r\\n.chat-panel {\\r\\n position: relative;\\r\\n min-width: 0;\\r\\n min-height: 0;\\r\\n height: 100%;\\r\\n display: grid;\\r\\n grid-template-rows: 72px minmax(0, 1fr) auto auto;\\r\\n overflow: hidden;\\r\\n background: #eef2f5;\\r\\n}\\r\\n\\r\\n.contact-panel {\\r\\n min-width: 0;\\r\\n min-height: 0;\\r\\n height: 100%;\\r\\n display: grid;\\r\\n grid-template-rows: 72px minmax(0, 1fr);\\r\\n overflow: auto;\\r\\n background: #eef2f5;\\r\\n}\\r\\n\\r\\n.contact-profile {\\r\\n align-self: start;\\r\\n width: min(520px, calc(100% - 48px));\\r\\n margin: 48px auto;\\r\\n display: grid;\\r\\n justify-items: center;\\r\\n gap: 18px;\\r\\n padding: 32px;\\r\\n background: var(--panel);\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 8px;\\r\\n}\\r\\n\\r\\n.profile-avatar {\\r\\n width: 76px;\\r\\n height: 76px;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n border-radius: 8px;\\r\\n background: #dce8f2;\\r\\n color: #24435c;\\r\\n font-size: 28px;\\r\\n font-weight: 800;\\r\\n}\\r\\n\\r\\n.profile-main {\\r\\n text-align: center;\\r\\n}\\r\\n\\r\\n.remark-row {\\r\\n min-width: 0;\\r\\n display: flex;\\r\\n align-items: center;\\r\\n justify-content: center;\\r\\n gap: 8px;\\r\\n}\\r\\n\\r\\n.profile-main h3 {\\r\\n margin: 0;\\r\\n font-size: 22px;\\r\\n overflow-wrap: anywhere;\\r\\n}\\r\\n\\r\\n.remark-edit-button {\\r\\n width: 32px;\\r\\n height: 32px;\\r\\n flex: 0 0 auto;\\r\\n font-size: 16px;\\r\\n}\\r\\n\\r\\n.profile-main p {\\r\\n margin: 6px 0 0;\\r\\n color: var(--muted);\\r\\n}\\r\\n\\r\\n.profile-fields {\\r\\n width: 100%;\\r\\n display: grid;\\r\\n gap: 12px;\\r\\n margin: 0;\\r\\n}\\r\\n\\r\\n.profile-fields div {\\r\\n display: grid;\\r\\n grid-template-columns: 80px minmax(0, 1fr);\\r\\n gap: 12px;\\r\\n padding: 10px 0;\\r\\n border-top: 1px solid var(--line);\\r\\n}\\r\\n\\r\\n.profile-fields dt {\\r\\n color: var(--muted);\\r\\n}\\r\\n\\r\\n.profile-fields dd {\\r\\n margin: 0;\\r\\n overflow-wrap: anywhere;\\r\\n}\\r\\n\\r\\n.profile-action {\\r\\n width: 160px;\\r\\n}\\r\\n\\r\\n.contact-panel .contact-modal-backdrop {\\r\\n position: fixed;\\r\\n}\\r\\n\\r\\n.header-actions {\\r\\n display: flex;\\r\\n gap: 8px;\\r\\n}\\r\\n\\r\\n.chat-header-actions {\\r\\n position: relative;\\r\\n}\\r\\n\\r\\n.chat-title-button {\\r\\n display: grid;\\r\\n grid-template-columns: 38px minmax(0, 1fr);\\r\\n align-items: center;\\r\\n gap: 10px;\\r\\n max-width: min(460px, 52vw);\\r\\n padding: 0;\\r\\n border: 0;\\r\\n background: transparent;\\r\\n color: var(--text);\\r\\n text-align: left;\\r\\n}\\r\\n\\r\\n.chat-title-avatar {\\r\\n width: 38px;\\r\\n height: 38px;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n border-radius: 8px;\\r\\n background: #dce8f2;\\r\\n color: #24435c;\\r\\n font-size: 16px;\\r\\n font-weight: 800;\\r\\n}\\r\\n\\r\\n.chat-title-copy {\\r\\n min-width: 0;\\r\\n}\\r\\n\\r\\n.chat-title-button h2 {\\r\\n overflow: hidden;\\r\\n text-overflow: ellipsis;\\r\\n white-space: nowrap;\\r\\n}\\r\\n\\r\\n.chat-title-button:hover h2 {\\r\\n color: var(--primary-strong);\\r\\n}\\r\\n\\r\\n.chat-more-menu {\\r\\n top: 38px;\\r\\n width: 150px;\\r\\n}\\r\\n\\r\\n.action-menu-anchor {\\r\\n position: relative;\\r\\n}\\r\\n\\r\\n.quick-action-menu {\\r\\n position: absolute;\\r\\n top: 40px;\\r\\n right: 0;\\r\\n z-index: 7;\\r\\n width: 168px;\\r\\n display: grid;\\r\\n padding: 6px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 8px;\\r\\n background: var(--panel);\\r\\n box-shadow: 0 14px 34px rgba(24, 33, 47, 0.16);\\r\\n}\\r\\n\\r\\n.quick-action-menu button {\\r\\n height: 34px;\\r\\n padding: 0 10px;\\r\\n border: 0;\\r\\n border-radius: 6px;\\r\\n background: transparent;\\r\\n color: var(--text);\\r\\n text-align: left;\\r\\n}\\r\\n\\r\\n.quick-action-menu button:hover {\\r\\n background: #edf2f5;\\r\\n}\\r\\n\\r\\n.quick-action-menu.chat-more-menu {\\r\\n top: 38px;\\r\\n width: 150px;\\r\\n}\\r\\n\\r\\n.conversation-action-panel {\\r\\n width: min(100%, 340px);\\r\\n max-height: calc(100dvh - 116px);\\r\\n overflow: auto;\\r\\n}\\r\\n\\r\\n.contact-picker {\\r\\n display: grid;\\r\\n gap: 10px;\\r\\n}\\r\\n\\r\\n.contact-picker > input {\\r\\n width: 100%;\\r\\n height: 38px;\\r\\n padding: 0 10px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n outline: none;\\r\\n}\\r\\n\\r\\n.contact-picker-list {\\r\\n max-height: 220px;\\r\\n overflow: auto;\\r\\n display: grid;\\r\\n gap: 6px;\\r\\n padding-right: 2px;\\r\\n}\\r\\n\\r\\n.contact-picker-row {\\r\\n width: 100%;\\r\\n min-height: 48px;\\r\\n display: grid;\\r\\n grid-template-columns: 22px 36px minmax(0, 1fr);\\r\\n align-items: center;\\r\\n gap: 8px;\\r\\n padding: 6px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n background: #fff;\\r\\n color: var(--text);\\r\\n text-align: left;\\r\\n}\\r\\n\\r\\n.contact-picker-row:hover,\\r\\n.contact-picker-row.selected {\\r\\n border-color: #9fd4cc;\\r\\n background: #edf8f5;\\r\\n}\\r\\n\\r\\n.contact-picker-row .conversation-avatar {\\r\\n width: 36px;\\r\\n height: 36px;\\r\\n border-radius: 6px;\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.contact-check {\\r\\n width: 20px;\\r\\n height: 20px;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 4px;\\r\\n color: var(--primary-strong);\\r\\n font-size: 13px;\\r\\n font-weight: 800;\\r\\n}\\r\\n\\r\\n.contact-picker-copy {\\r\\n min-width: 0;\\r\\n display: grid;\\r\\n gap: 2px;\\r\\n}\\r\\n\\r\\n.contact-picker-copy strong,\\r\\n.contact-picker-copy small {\\r\\n overflow: hidden;\\r\\n text-overflow: ellipsis;\\r\\n white-space: nowrap;\\r\\n}\\r\\n\\r\\n.contact-picker-copy small {\\r\\n color: var(--muted);\\r\\n font-size: 12px;\\r\\n}\\r\\n\\r\\n.contact-picker-empty {\\r\\n min-height: 64px;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n color: var(--muted);\\r\\n border: 1px dashed var(--line);\\r\\n border-radius: 6px;\\r\\n}\\r\\n\\r\\n.group-dialog {\\r\\n width: min(420px, calc(100% - 32px));\\r\\n max-height: calc(100dvh - 116px);\\r\\n display: grid;\\r\\n gap: 16px;\\r\\n padding: 18px;\\r\\n overflow: auto;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 8px;\\r\\n background: var(--panel);\\r\\n box-shadow: 0 18px 40px rgba(24, 33, 47, 0.18);\\r\\n}\\r\\n\\r\\n.group-dialog header,\\r\\n.group-dialog footer {\\r\\n display: flex;\\r\\n align-items: center;\\r\\n justify-content: space-between;\\r\\n gap: 12px;\\r\\n}\\r\\n\\r\\n.group-dialog h3 {\\r\\n margin: 0;\\r\\n font-size: 18px;\\r\\n}\\r\\n\\r\\n.single-profile-dialog {\\r\\n width: min(420px, calc(100% - 32px));\\r\\n}\\r\\n\\r\\n.single-profile-summary {\\r\\n display: grid;\\r\\n justify-items: center;\\r\\n gap: 10px;\\r\\n}\\r\\n\\r\\n.single-profile-summary strong {\\r\\n max-width: 100%;\\r\\n overflow-wrap: anywhere;\\r\\n font-size: 18px;\\r\\n}\\r\\n\\r\\n.single-profile-footer {\\r\\n align-items: center;\\r\\n justify-content: flex-end;\\r\\n}\\r\\n\\r\\n.single-profile-footer .inline-save-status {\\r\\n margin-right: auto;\\r\\n}\\r\\n\\r\\n.message-detail-dialog {\\r\\n width: min(560px, calc(100% - 32px));\\r\\n}\\r\\n\\r\\n.message-detail-meta div {\\r\\n grid-template-columns: 88px minmax(0, 1fr);\\r\\n}\\r\\n\\r\\n.message-detail-muted {\\r\\n color: var(--muted);\\r\\n}\\r\\n\\r\\n.message-detail-summary {\\r\\n display: flex;\\r\\n flex-wrap: wrap;\\r\\n gap: 8px;\\r\\n}\\r\\n\\r\\n.message-detail-summary span {\\r\\n min-height: 30px;\\r\\n display: inline-flex;\\r\\n align-items: center;\\r\\n gap: 4px;\\r\\n padding: 0 10px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n background: #f8fbfb;\\r\\n color: var(--muted);\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.message-detail-summary strong {\\r\\n color: var(--text);\\r\\n}\\r\\n\\r\\n.message-detail-groups {\\r\\n display: grid;\\r\\n gap: 14px;\\r\\n}\\r\\n\\r\\n.message-detail-section {\\r\\n display: grid;\\r\\n gap: 8px;\\r\\n}\\r\\n\\r\\n.message-detail-section header {\\r\\n min-height: 28px;\\r\\n display: flex;\\r\\n align-items: center;\\r\\n justify-content: space-between;\\r\\n gap: 12px;\\r\\n border-bottom: 1px solid var(--line);\\r\\n}\\r\\n\\r\\n.message-detail-section header strong {\\r\\n font-size: 14px;\\r\\n}\\r\\n\\r\\n.message-detail-section header span {\\r\\n color: var(--muted);\\r\\n font-size: 12px;\\r\\n}\\r\\n\\r\\n.message-detail-member-list {\\r\\n display: grid;\\r\\n overflow: hidden;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n background: #fff;\\r\\n}\\r\\n\\r\\n.message-detail-member-row {\\r\\n display: grid;\\r\\n grid-template-columns: 34px minmax(0, 1fr) auto auto;\\r\\n align-items: center;\\r\\n gap: 8px;\\r\\n min-height: 48px;\\r\\n padding: 7px 8px;\\r\\n border-top: 1px solid var(--line);\\r\\n}\\r\\n\\r\\n.message-detail-member-row:first-child {\\r\\n border-top: 0;\\r\\n}\\r\\n\\r\\n.message-detail-member-row .conversation-avatar {\\r\\n width: 34px;\\r\\n height: 34px;\\r\\n border-radius: 6px;\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.message-detail-member-main {\\r\\n min-width: 0;\\r\\n display: grid;\\r\\n gap: 2px;\\r\\n}\\r\\n\\r\\n.message-detail-member-main strong,\\r\\n.message-detail-member-main small {\\r\\n overflow: hidden;\\r\\n text-overflow: ellipsis;\\r\\n white-space: nowrap;\\r\\n}\\r\\n\\r\\n.message-detail-member-main strong {\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.message-detail-member-main small,\\r\\n.message-detail-member-time {\\r\\n color: var(--muted);\\r\\n font-size: 12px;\\r\\n}\\r\\n\\r\\n.message-detail-member-time {\\r\\n white-space: nowrap;\\r\\n}\\r\\n\\r\\n.message-detail-role {\\r\\n min-height: 22px;\\r\\n display: inline-flex;\\r\\n align-items: center;\\r\\n padding: 0 7px;\\r\\n border: 1px solid #c7d9e5;\\r\\n border-radius: 6px;\\r\\n color: #416272;\\r\\n font-size: 12px;\\r\\n white-space: nowrap;\\r\\n}\\r\\n\\r\\n.group-dialog label {\\r\\n display: grid;\\r\\n gap: 8px;\\r\\n color: var(--muted);\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.group-dialog input {\\r\\n width: 100%;\\r\\n height: 42px;\\r\\n padding: 0 12px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n outline: none;\\r\\n}\\r\\n\\r\\n.group-dialog textarea,\\r\\n.group-dialog select {\\r\\n width: 100%;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n outline: none;\\r\\n background: #fff;\\r\\n color: var(--text);\\r\\n}\\r\\n\\r\\n.group-dialog textarea {\\r\\n min-height: 86px;\\r\\n resize: vertical;\\r\\n padding: 10px 12px;\\r\\n line-height: 1.5;\\r\\n}\\r\\n\\r\\n.group-dialog select {\\r\\n height: 42px;\\r\\n padding: 0 10px;\\r\\n}\\r\\n\\r\\n.group-dialog input:focus {\\r\\n border-color: var(--primary);\\r\\n box-shadow: 0 0 0 3px rgba(23, 125, 114, 0.12);\\r\\n}\\r\\n\\r\\n.group-dialog textarea:focus,\\r\\n.group-dialog select:focus {\\r\\n border-color: var(--primary);\\r\\n box-shadow: 0 0 0 3px rgba(23, 125, 114, 0.12);\\r\\n}\\r\\n\\r\\n.group-profile {\\r\\n display: grid;\\r\\n gap: 10px;\\r\\n margin: 0;\\r\\n}\\r\\n\\r\\n.group-profile div {\\r\\n display: grid;\\r\\n grid-template-columns: 78px minmax(0, 1fr);\\r\\n gap: 12px;\\r\\n padding: 10px 0;\\r\\n border-top: 1px solid var(--line);\\r\\n}\\r\\n\\r\\n.group-profile dt {\\r\\n color: var(--muted);\\r\\n}\\r\\n\\r\\n.group-profile dd {\\r\\n margin: 0;\\r\\n overflow-wrap: anywhere;\\r\\n}\\r\\n\\r\\n.group-loading {\\r\\n min-height: 96px;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n color: var(--muted);\\r\\n}\\r\\n\\r\\n.group-member-list {\\r\\n display: grid;\\r\\n gap: 8px;\\r\\n}\\r\\n\\r\\n.group-member-row {\\r\\n display: grid;\\r\\n grid-template-columns: 42px minmax(0, 1fr);\\r\\n align-items: center;\\r\\n gap: 10px;\\r\\n min-height: 54px;\\r\\n padding: 8px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n background: #fff;\\r\\n}\\r\\n\\r\\n.group-member-row.selectable {\\r\\n cursor: pointer;\\r\\n}\\r\\n\\r\\n.group-member-row.with-check {\\r\\n grid-template-columns: 24px 42px minmax(0, 1fr);\\r\\n}\\r\\n\\r\\n.group-member-row.selectable:hover,\\r\\n.group-member-row.selectable.selected {\\r\\n border-color: #9fd4cc;\\r\\n background: #edf8f5;\\r\\n}\\r\\n\\r\\n.group-member-row input[type=\\\"checkbox\\\"] {\\r\\n justify-self: center;\\r\\n width: 16px;\\r\\n height: 16px;\\r\\n}\\r\\n\\r\\n.group-member-row span:last-child {\\r\\n min-width: 0;\\r\\n display: grid;\\r\\n gap: 3px;\\r\\n}\\r\\n\\r\\n.group-member-row strong,\\r\\n.group-member-row small {\\r\\n overflow: hidden;\\r\\n text-overflow: ellipsis;\\r\\n white-space: nowrap;\\r\\n}\\r\\n\\r\\n.group-member-row small {\\r\\n color: var(--muted);\\r\\n font-size: 12px;\\r\\n}\\r\\n\\r\\n.group-management-dialog {\\r\\n width: min(520px, calc(100% - 32px));\\r\\n}\\r\\n\\r\\n.group-management-tabs {\\r\\n display: flex;\\r\\n flex-wrap: wrap;\\r\\n gap: 8px;\\r\\n}\\r\\n\\r\\n.group-management-tabs button {\\r\\n height: 34px;\\r\\n padding: 0 10px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n background: #fff;\\r\\n color: var(--text);\\r\\n}\\r\\n\\r\\n.group-management-tabs button.active,\\r\\n.group-management-tabs button:hover {\\r\\n border-color: var(--primary);\\r\\n background: #e8f4f2;\\r\\n color: var(--primary-strong);\\r\\n font-weight: 700;\\r\\n}\\r\\n\\r\\n.group-manage-section {\\r\\n display: grid;\\r\\n gap: 14px;\\r\\n}\\r\\n\\r\\n.group-manage-section footer {\\r\\n justify-content: flex-end;\\r\\n}\\r\\n\\r\\n.mute-save-footer {\\r\\n align-items: center;\\r\\n gap: 10px;\\r\\n}\\r\\n\\r\\n.inline-save-status {\\r\\n color: var(--primary-strong);\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.inline-save-status.error {\\r\\n color: var(--accent);\\r\\n}\\r\\n\\r\\n.mute-time-grid {\\r\\n display: grid;\\r\\n grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);\\r\\n gap: 10px;\\r\\n}\\r\\n\\r\\n.message-list {\\r\\n min-height: 0;\\r\\n overflow: auto;\\r\\n overscroll-behavior: contain;\\r\\n padding: 24px;\\r\\n}\\r\\n\\r\\n.message-row {\\r\\n display: flex;\\r\\n align-items: flex-end;\\r\\n gap: 8px;\\r\\n margin-bottom: 14px;\\r\\n}\\r\\n\\r\\n.message-row.out {\\r\\n justify-content: flex-end;\\r\\n}\\r\\n\\r\\n.message-sender-avatar {\\r\\n width: 34px;\\r\\n height: 34px;\\r\\n flex: 0 0 auto;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n overflow: hidden;\\r\\n padding: 0;\\r\\n border: 0;\\r\\n border-radius: 8px;\\r\\n background: #dce8f2;\\r\\n color: #24435c;\\r\\n font-size: 14px;\\r\\n font-weight: 800;\\r\\n}\\r\\n\\r\\n.message-sender-avatar img {\\r\\n width: 100%;\\r\\n height: 100%;\\r\\n -o-object-fit: cover;\\r\\n object-fit: cover;\\r\\n}\\r\\n\\r\\n.message-stack {\\r\\n max-width: min(68%, 720px);\\r\\n display: grid;\\r\\n justify-items: start;\\r\\n gap: 4px;\\r\\n}\\r\\n\\r\\n.message-row.out .message-stack {\\r\\n justify-items: end;\\r\\n}\\r\\n\\r\\n.message-sender-name {\\r\\n min-height: 18px;\\r\\n max-width: 100%;\\r\\n padding: 0;\\r\\n border: 0;\\r\\n background: transparent;\\r\\n color: var(--muted);\\r\\n font-size: 12px;\\r\\n font-weight: 700;\\r\\n text-align: left;\\r\\n overflow: hidden;\\r\\n text-overflow: ellipsis;\\r\\n white-space: nowrap;\\r\\n}\\r\\n\\r\\n.message-sender-name:hover {\\r\\n color: var(--primary-strong);\\r\\n}\\r\\n\\r\\n.message-sender-role {\\r\\n margin-left: 4px;\\r\\n color: var(--primary-strong);\\r\\n font-size: 11px;\\r\\n}\\r\\n\\r\\n.message-row.selected .message-bubble {\\r\\n box-shadow: 0 0 0 2px rgba(24, 138, 121, 0.26);\\r\\n}\\r\\n\\r\\n.message-row.attention .message-bubble {\\r\\n animation: message-attention 1.4s ease-in-out 2;\\r\\n box-shadow: 0 0 0 3px rgba(24, 138, 121, 0.3);\\r\\n}\\r\\n\\r\\n@keyframes message-attention {\\r\\n 0%,\\r\\n 100% {\\r\\n box-shadow: 0 0 0 2px rgba(24, 138, 121, 0.16);\\r\\n }\\r\\n\\r\\n 45% {\\r\\n box-shadow: 0 0 0 5px rgba(24, 138, 121, 0.36);\\r\\n }\\r\\n}\\r\\n\\r\\n.message-select {\\r\\n width: 24px;\\r\\n height: 24px;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n}\\r\\n\\r\\n.message-select input {\\r\\n width: 16px;\\r\\n height: 16px;\\r\\n}\\r\\n\\r\\n.message-bubble {\\r\\n position: relative;\\r\\n max-width: min(68%, 720px);\\r\\n padding: 10px 12px 8px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 8px;\\r\\n background: var(--bubble-in);\\r\\n box-shadow: 0 2px 8px rgba(30, 40, 50, 0.05);\\r\\n}\\r\\n\\r\\n.message-stack .message-bubble {\\r\\n max-width: 100%;\\r\\n}\\r\\n\\r\\n.message-row.out .message-bubble {\\r\\n background: var(--bubble-out);\\r\\n border-color: #bfe4dc;\\r\\n}\\r\\n\\r\\n.message-bubble:hover {\\r\\n border-color: #9fd4cc;\\r\\n}\\r\\n\\r\\n.message-bubble.role-bubble-owner,\\r\\n.message-bubble.role-bubble-admin {\\r\\n overflow: hidden;\\r\\n}\\r\\n\\r\\n.message-bubble.role-bubble-owner::after,\\r\\n.message-bubble.role-bubble-admin::after {\\r\\n content: \\\"\\\";\\r\\n position: absolute;\\r\\n top: 0;\\r\\n right: 0;\\r\\n width: 0;\\r\\n height: 0;\\r\\n border-top: 16px solid #d79d22;\\r\\n border-left: 16px solid transparent;\\r\\n}\\r\\n\\r\\n.message-bubble.role-bubble-admin::after {\\r\\n border-top-color: #178a79;\\r\\n}\\r\\n\\r\\n.message-bubble p {\\r\\n margin: 0;\\r\\n line-height: 1.5;\\r\\n white-space: pre-wrap;\\r\\n overflow-wrap: anywhere;\\r\\n}\\r\\n\\r\\n.message-image {\\r\\n padding: 0;\\r\\n border: 0;\\r\\n display: block;\\r\\n max-width: min(280px, 62vw);\\r\\n overflow: hidden;\\r\\n border-radius: 6px;\\r\\n background: #edf2f5;\\r\\n cursor: zoom-in;\\r\\n}\\r\\n\\r\\n.message-image + p,\\r\\n.message-file + p {\\r\\n margin-top: 8px;\\r\\n}\\r\\n\\r\\n.message-image img {\\r\\n display: block;\\r\\n max-width: 100%;\\r\\n max-height: 320px;\\r\\n -o-object-fit: contain;\\r\\n object-fit: contain;\\r\\n}\\r\\n\\r\\n.image-viewer-backdrop {\\r\\n position: fixed;\\r\\n inset: 0;\\r\\n z-index: 80;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n padding: 24px;\\r\\n background: rgba(12, 18, 26, 0.58);\\r\\n}\\r\\n\\r\\n.image-viewer-dialog {\\r\\n width: min(880px, calc(100vw - 48px));\\r\\n height: min(640px, calc(100vh - 48px));\\r\\n min-width: 360px;\\r\\n min-height: 280px;\\r\\n max-width: calc(100vw - 48px);\\r\\n max-height: calc(100vh - 48px);\\r\\n display: grid;\\r\\n grid-template-rows: 52px minmax(0, 1fr);\\r\\n overflow: hidden;\\r\\n resize: both;\\r\\n border: 1px solid rgba(255, 255, 255, 0.18);\\r\\n border-radius: 8px;\\r\\n background: #101820;\\r\\n box-shadow: 0 24px 70px rgba(0, 0, 0, 0.36);\\r\\n}\\r\\n\\r\\n.image-viewer-toolbar {\\r\\n min-width: 0;\\r\\n display: flex;\\r\\n align-items: center;\\r\\n justify-content: flex-end;\\r\\n gap: 8px;\\r\\n padding: 8px 10px;\\r\\n border-bottom: 1px solid rgba(255, 255, 255, 0.12);\\r\\n background: rgba(24, 33, 47, 0.94);\\r\\n}\\r\\n\\r\\n.image-viewer-title {\\r\\n min-width: 0;\\r\\n flex: 1;\\r\\n overflow: hidden;\\r\\n color: #f8fafc;\\r\\n font-size: 13px;\\r\\n font-weight: 700;\\r\\n text-overflow: ellipsis;\\r\\n white-space: nowrap;\\r\\n}\\r\\n\\r\\n.image-viewer-toolbar button {\\r\\n width: 36px;\\r\\n height: 36px;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n border: 0;\\r\\n border-radius: 6px;\\r\\n background: transparent;\\r\\n color: #f8fafc;\\r\\n font-size: 17px;\\r\\n font-weight: 700;\\r\\n line-height: 1;\\r\\n}\\r\\n\\r\\n.image-viewer-toolbar button:hover {\\r\\n background: rgba(255, 255, 255, 0.14);\\r\\n}\\r\\n\\r\\n.image-viewer-stage {\\r\\n min-width: 0;\\r\\n min-height: 0;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n overflow: hidden;\\r\\n background: #0c121a;\\r\\n cursor: default;\\r\\n touch-action: none;\\r\\n}\\r\\n\\r\\n.image-viewer-stage.zoomed {\\r\\n cursor: grab;\\r\\n}\\r\\n\\r\\n.image-viewer-stage.dragging {\\r\\n cursor: grabbing;\\r\\n}\\r\\n\\r\\n.image-viewer-img {\\r\\n max-width: calc(100% - 32px);\\r\\n max-height: calc(100% - 32px);\\r\\n -o-object-fit: contain;\\r\\n object-fit: contain;\\r\\n transform-origin: center center;\\r\\n transition: transform 0.08s ease-out;\\r\\n -webkit-user-select: none;\\r\\n -moz-user-select: none;\\r\\n user-select: none;\\r\\n pointer-events: none;\\r\\n}\\r\\n\\r\\n.image-viewer-stage.dragging .image-viewer-img {\\r\\n transition: none;\\r\\n}\\r\\n\\r\\n.sender-profile-backdrop {\\r\\n position: fixed;\\r\\n inset: 0;\\r\\n z-index: 84;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n padding: 24px;\\r\\n background: rgba(24, 33, 47, 0.32);\\r\\n}\\r\\n\\r\\n.sender-profile-dialog {\\r\\n width: min(380px, calc(100vw - 32px));\\r\\n display: grid;\\r\\n gap: 16px;\\r\\n padding: 18px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 8px;\\r\\n background: var(--panel);\\r\\n box-shadow: 0 18px 40px rgba(24, 33, 47, 0.18);\\r\\n}\\r\\n\\r\\n.sender-profile-dialog header {\\r\\n display: flex;\\r\\n align-items: center;\\r\\n justify-content: space-between;\\r\\n gap: 12px;\\r\\n}\\r\\n\\r\\n.sender-profile-main {\\r\\n display: grid;\\r\\n justify-items: center;\\r\\n gap: 8px;\\r\\n text-align: center;\\r\\n}\\r\\n\\r\\n.sender-profile-main strong {\\r\\n max-width: 100%;\\r\\n overflow-wrap: anywhere;\\r\\n font-size: 18px;\\r\\n}\\r\\n\\r\\n.sender-profile-main span {\\r\\n color: var(--primary-strong);\\r\\n font-size: 12px;\\r\\n font-weight: 700;\\r\\n}\\r\\n\\r\\n.sender-profile-fields {\\r\\n display: grid;\\r\\n gap: 8px;\\r\\n margin: 0;\\r\\n}\\r\\n\\r\\n.sender-profile-fields div {\\r\\n display: grid;\\r\\n grid-template-columns: 72px minmax(0, 1fr);\\r\\n gap: 10px;\\r\\n padding-top: 8px;\\r\\n border-top: 1px solid var(--line);\\r\\n}\\r\\n\\r\\n.sender-profile-fields dt {\\r\\n color: var(--muted);\\r\\n}\\r\\n\\r\\n.sender-profile-fields dd {\\r\\n margin: 0;\\r\\n overflow-wrap: anywhere;\\r\\n}\\r\\n\\r\\n.sender-profile-alias {\\r\\n display: grid;\\r\\n gap: 8px;\\r\\n color: var(--muted);\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.sender-profile-alias input {\\r\\n width: 100%;\\r\\n height: 40px;\\r\\n padding: 0 12px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 6px;\\r\\n outline: none;\\r\\n background: #fff;\\r\\n color: var(--text);\\r\\n}\\r\\n\\r\\n.sender-profile-alias input:focus {\\r\\n border-color: var(--primary);\\r\\n box-shadow: 0 0 0 3px rgba(23, 125, 114, 0.12);\\r\\n}\\r\\n\\r\\n.sender-profile-actions {\\r\\n display: flex;\\r\\n align-items: center;\\r\\n justify-content: flex-end;\\r\\n gap: 8px;\\r\\n}\\r\\n\\r\\n.sender-profile-actions .inline-save-status {\\r\\n margin-right: auto;\\r\\n}\\r\\n\\r\\n@media (max-width: 480px) {\\r\\n .image-viewer-backdrop {\\r\\n padding: 12px;\\r\\n }\\r\\n\\r\\n .image-viewer-dialog {\\r\\n width: calc(100vw - 24px);\\r\\n height: min(560px, calc(100vh - 24px));\\r\\n min-width: 280px;\\r\\n min-height: 240px;\\r\\n max-width: calc(100vw - 24px);\\r\\n max-height: calc(100vh - 24px);\\r\\n }\\r\\n\\r\\n .image-viewer-toolbar {\\r\\n gap: 4px;\\r\\n padding: 6px;\\r\\n }\\r\\n\\r\\n .image-viewer-toolbar button {\\r\\n width: 32px;\\r\\n height: 32px;\\r\\n font-size: 15px;\\r\\n }\\r\\n\\r\\n .message-detail-member-row {\\r\\n grid-template-columns: 34px minmax(0, 1fr);\\r\\n }\\r\\n\\r\\n .message-detail-member-time,\\r\\n .message-detail-role {\\r\\n grid-column: 2;\\r\\n justify-self: start;\\r\\n }\\r\\n\\r\\n .avatar-cropper-panel {\\r\\n width: calc(100vw - 24px);\\r\\n max-height: calc(100dvh - 24px);\\r\\n }\\r\\n\\r\\n .avatar-cropper-layout {\\r\\n grid-template-columns: minmax(0, 1fr);\\r\\n }\\r\\n\\r\\n .avatar-cropper-stage {\\r\\n height: 300px;\\r\\n min-height: 300px;\\r\\n }\\r\\n\\r\\n .avatar-cropper-controls {\\r\\n grid-template-columns: minmax(0, 1fr) auto;\\r\\n align-items: center;\\r\\n }\\r\\n\\r\\n .avatar-cropper-tools {\\r\\n grid-template-columns: repeat(3, minmax(0, 1fr));\\r\\n }\\r\\n\\r\\n .avatar-cropper-preview {\\r\\n width: 82px;\\r\\n height: 82px;\\r\\n }\\r\\n\\r\\n .avatar-cropper-controls footer {\\r\\n grid-column: 1 / -1;\\r\\n }\\r\\n\\r\\n .settings-panel {\\r\\n width: calc(100vw - 24px);\\r\\n max-height: calc(100dvh - 24px);\\r\\n }\\r\\n\\r\\n .settings-layout {\\r\\n grid-template-columns: minmax(0, 1fr);\\r\\n }\\r\\n\\r\\n .settings-menu {\\r\\n grid-auto-flow: column;\\r\\n grid-auto-columns: minmax(120px, 1fr);\\r\\n overflow-x: auto;\\r\\n padding-bottom: 2px;\\r\\n }\\r\\n\\r\\n .settings-password-form footer .inline-save-status {\\r\\n flex: 1 1 100%;\\r\\n }\\r\\n}\\r\\n\\r\\n.message-file {\\r\\n width: min(280px, 62vw);\\r\\n display: grid;\\r\\n grid-template-columns: 38px minmax(0, 1fr);\\r\\n align-items: center;\\r\\n gap: 10px;\\r\\n padding: 10px;\\r\\n border: 1px solid #d6e2e7;\\r\\n border-radius: 6px;\\r\\n background: #f8fbfc;\\r\\n color: var(--text);\\r\\n text-decoration: none;\\r\\n}\\r\\n\\r\\n.message-file:hover {\\r\\n border-color: #9fd4cc;\\r\\n background: #edf8f5;\\r\\n}\\r\\n\\r\\n.message-file-icon {\\r\\n width: 38px;\\r\\n height: 38px;\\r\\n display: grid;\\r\\n place-items: center;\\r\\n border-radius: 6px;\\r\\n background: #e3eff2;\\r\\n color: var(--primary-strong);\\r\\n font-size: 18px;\\r\\n font-weight: 800;\\r\\n}\\r\\n\\r\\n.message-file-main {\\r\\n min-width: 0;\\r\\n display: grid;\\r\\n gap: 3px;\\r\\n}\\r\\n\\r\\n.message-file-main strong,\\r\\n.message-file-main small {\\r\\n overflow: hidden;\\r\\n text-overflow: ellipsis;\\r\\n white-space: nowrap;\\r\\n}\\r\\n\\r\\n.message-file-main strong {\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.message-file-main small {\\r\\n color: var(--muted);\\r\\n font-size: 11px;\\r\\n}\\r\\n\\r\\n.message-bubble time {\\r\\n display: flex;\\r\\n align-items: center;\\r\\n justify-content: flex-end;\\r\\n flex-wrap: wrap;\\r\\n gap: 4px;\\r\\n margin-top: 4px;\\r\\n color: var(--muted);\\r\\n font-size: 11px;\\r\\n text-align: right;\\r\\n}\\r\\n\\r\\n.recall-mark {\\r\\n margin-right: auto;\\r\\n color: #d93025;\\r\\n font-size: 11px;\\r\\n font-weight: 700;\\r\\n line-height: 1;\\r\\n}\\r\\n\\r\\n.edit-mark {\\r\\n color: #7a8792;\\r\\n font-size: 11px;\\r\\n font-weight: 600;\\r\\n line-height: 1;\\r\\n}\\r\\n\\r\\n.delivery-mark {\\r\\n display: inline-flex;\\r\\n align-items: center;\\r\\n justify-content: flex-end;\\r\\n min-width: 14px;\\r\\n color: #8f9aa3;\\r\\n font-size: 14px;\\r\\n font-weight: 800;\\r\\n letter-spacing: 0;\\r\\n line-height: 1;\\r\\n}\\r\\n\\r\\n.delivery-mark-delivered,\\r\\n.delivery-mark-read {\\r\\n min-width: 19px;\\r\\n}\\r\\n\\r\\n.delivery-mark-read {\\r\\n color: #1aa66a;\\r\\n}\\r\\n\\r\\n.favorite-mark {\\r\\n color: #c79520;\\r\\n font-size: 12px;\\r\\n line-height: 1;\\r\\n}\\r\\n\\r\\n.delivery-check {\\r\\n display: inline-block;\\r\\n}\\r\\n\\r\\n.delivery-check-second {\\r\\n margin-left: -5px;\\r\\n}\\r\\n\\r\\n.delivery-error {\\r\\n color: #c0392b;\\r\\n}\\r\\n\\r\\n.message-context-menu {\\r\\n position: fixed;\\r\\n z-index: 30;\\r\\n width: 136px;\\r\\n padding: 6px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 8px;\\r\\n background: var(--panel);\\r\\n box-shadow: 0 16px 40px rgba(24, 33, 47, 0.18);\\r\\n}\\r\\n\\r\\n.message-context-menu button {\\r\\n width: 100%;\\r\\n min-height: 32px;\\r\\n padding: 0 10px;\\r\\n border: 0;\\r\\n border-radius: 6px;\\r\\n background: transparent;\\r\\n color: var(--text);\\r\\n text-align: left;\\r\\n}\\r\\n\\r\\n.message-context-menu button:hover {\\r\\n background: #eaf3f1;\\r\\n}\\r\\n\\r\\n.selection-toolbar {\\r\\n min-height: 42px;\\r\\n display: flex;\\r\\n align-items: center;\\r\\n gap: 8px;\\r\\n padding: 8px 16px;\\r\\n border-top: 1px solid var(--line);\\r\\n background: #f7fafc;\\r\\n}\\r\\n\\r\\n.selection-toolbar span {\\r\\n flex: 1;\\r\\n color: var(--muted);\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.selection-toolbar button {\\r\\n min-height: 30px;\\r\\n padding: 0 10px;\\r\\n border-radius: 6px;\\r\\n}\\r\\n\\r\\n.forward-dialog,\\r\\n.edit-dialog {\\r\\n width: min(100%, 340px);\\r\\n max-height: min(520px, calc(100vh - 130px));\\r\\n display: grid;\\r\\n gap: 4px;\\r\\n padding: 14px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 8px;\\r\\n background: var(--panel);\\r\\n box-shadow: 0 18px 40px rgba(24, 33, 47, 0.18);\\r\\n overflow: auto;\\r\\n}\\r\\n\\r\\n.forward-dialog header,\\r\\n.edit-dialog header,\\r\\n.forward-target {\\r\\n display: flex;\\r\\n align-items: center;\\r\\n gap: 10px;\\r\\n}\\r\\n\\r\\n.forward-dialog header,\\r\\n.edit-dialog header {\\r\\n justify-content: space-between;\\r\\n margin-bottom: 6px;\\r\\n}\\r\\n\\r\\n.forward-dialog h3,\\r\\n.edit-dialog h3 {\\r\\n margin: 0;\\r\\n font-size: 16px;\\r\\n}\\r\\n\\r\\n.edit-dialog {\\r\\n width: min(100%, 420px);\\r\\n gap: 12px;\\r\\n}\\r\\n\\r\\n.edit-dialog textarea {\\r\\n width: 100%;\\r\\n min-height: 132px;\\r\\n resize: vertical;\\r\\n border-radius: 8px;\\r\\n border: 1px solid var(--line);\\r\\n padding: 10px;\\r\\n background: #f9fbfc;\\r\\n color: var(--text);\\r\\n line-height: 1.5;\\r\\n}\\r\\n\\r\\n.edit-dialog textarea:focus {\\r\\n border-color: var(--accent);\\r\\n outline: 0;\\r\\n box-shadow: 0 0 0 3px rgba(24, 138, 121, 0.14);\\r\\n}\\r\\n\\r\\n.edit-dialog footer {\\r\\n display: flex;\\r\\n justify-content: flex-end;\\r\\n gap: 8px;\\r\\n}\\r\\n\\r\\n.edit-dialog footer button {\\r\\n min-height: 34px;\\r\\n padding: 0 14px;\\r\\n border-radius: 8px;\\r\\n}\\r\\n\\r\\n.forward-target {\\r\\n min-height: 52px;\\r\\n padding: 6px;\\r\\n border: 0;\\r\\n border-radius: 8px;\\r\\n background: transparent;\\r\\n color: var(--text);\\r\\n text-align: left;\\r\\n}\\r\\n\\r\\n.forward-target:hover {\\r\\n background: #eaf3f1;\\r\\n}\\r\\n\\r\\n.chat-toast {\\r\\n position: absolute;\\r\\n left: 50%;\\r\\n bottom: 132px;\\r\\n z-index: 20;\\r\\n transform: translateX(-50%);\\r\\n padding: 8px 12px;\\r\\n border-radius: 8px;\\r\\n background: rgba(35, 45, 58, 0.92);\\r\\n color: #fff;\\r\\n font-size: 13px;\\r\\n pointer-events: none;\\r\\n}\\r\\n\\r\\n.chat-toast.error {\\r\\n background: rgba(192, 57, 43, 0.94);\\r\\n}\\r\\n\\r\\n.message-composer {\\r\\n position: relative;\\r\\n min-height: 0;\\r\\n border-top: 1px solid var(--line);\\r\\n background: var(--panel);\\r\\n}\\r\\n\\r\\n.message-composer::after {\\r\\n content: \\\"\\\";\\r\\n position: absolute;\\r\\n inset: 6px;\\r\\n z-index: 4;\\r\\n border: 2px dashed transparent;\\r\\n border-radius: 8px;\\r\\n pointer-events: none;\\r\\n transition: border-color 0.12s ease, background-color 0.12s ease;\\r\\n}\\r\\n\\r\\n.message-composer.drop-active::after {\\r\\n border-color: var(--primary);\\r\\n background: rgba(23, 125, 114, 0.08);\\r\\n}\\r\\n\\r\\n.quote-preview {\\r\\n display: flex;\\r\\n align-items: center;\\r\\n gap: 8px;\\r\\n padding: 8px 16px 0;\\r\\n color: var(--muted);\\r\\n font-size: 13px;\\r\\n}\\r\\n\\r\\n.quote-preview span {\\r\\n min-width: 0;\\r\\n flex: 1;\\r\\n overflow: hidden;\\r\\n text-overflow: ellipsis;\\r\\n white-space: nowrap;\\r\\n border-left: 3px solid var(--accent);\\r\\n padding-left: 8px;\\r\\n}\\r\\n\\r\\n.quote-preview strong {\\r\\n margin-right: 6px;\\r\\n color: var(--text);\\r\\n}\\r\\n\\r\\n.quote-preview button {\\r\\n width: 26px;\\r\\n height: 26px;\\r\\n border: 0;\\r\\n border-radius: 6px;\\r\\n background: transparent;\\r\\n color: var(--muted);\\r\\n font-size: 18px;\\r\\n}\\r\\n\\r\\n.quote-preview button:hover {\\r\\n background: #edf2f5;\\r\\n}\\r\\n\\r\\n.composer-tools {\\r\\n position: relative;\\r\\n display: flex;\\r\\n gap: 4px;\\r\\n padding: 8px 16px 0;\\r\\n}\\r\\n\\r\\n.composer-tools input[type=\\\"file\\\"] {\\r\\n display: none;\\r\\n}\\r\\n\\r\\n.composer-tools button {\\r\\n width: 32px;\\r\\n height: 32px;\\r\\n border: 0;\\r\\n border-radius: 6px;\\r\\n background: transparent;\\r\\n color: var(--muted);\\r\\n font-size: 18px;\\r\\n}\\r\\n\\r\\n.composer-tools button:hover {\\r\\n background: #edf2f5;\\r\\n}\\r\\n\\r\\n.emoji-picker {\\r\\n position: absolute;\\r\\n left: 16px;\\r\\n bottom: 38px;\\r\\n z-index: 12;\\r\\n width: 256px;\\r\\n display: grid;\\r\\n grid-template-columns: repeat(8, 1fr);\\r\\n gap: 2px;\\r\\n padding: 8px;\\r\\n border: 1px solid var(--line);\\r\\n border-radius: 8px;\\r\\n background: var(--panel);\\r\\n box-shadow: 0 14px 34px rgba(24, 33, 47, 0.16);\\r\\n}\\r\\n\\r\\n.emoji-picker button {\\r\\n width: 28px;\\r\\n height: 28px;\\r\\n border-radius: 6px;\\r\\n font-size: 17px;\\r\\n}\\r\\n\\r\\n.message-composer textarea {\\r\\n display: block;\\r\\n min-height: 86px;\\r\\n max-height: 180px;\\r\\n padding: 12px 16px;\\r\\n border: 0;\\r\\n border-radius: 0;\\r\\n resize: none;\\r\\n box-shadow: none;\\r\\n}\\r\\n\\r\\n.composer-footer {\\r\\n display: flex;\\r\\n align-items: center;\\r\\n justify-content: space-between;\\r\\n gap: 12px;\\r\\n padding: 0 16px 14px;\\r\\n color: var(--muted);\\r\\n font-size: 12px;\\r\\n}\\r\\n\\r\\n.send-button {\\r\\n min-width: 72px;\\r\\n height: 34px;\\r\\n}\\r\\n\\r\\n@media (max-width: 760px) {\\r\\n .app-shell {\\r\\n grid-template-columns: 56px minmax(0, 1fr);\\r\\n }\\r\\n\\r\\n .conversation-list {\\r\\n display: none;\\r\\n }\\r\\n\\r\\n .message-bubble {\\r\\n max-width: 86%;\\r\\n }\\r\\n}\\r\\n\", \"\"]);\n// Exports\n/* harmony default export */ __webpack_exports__[\"default\"] = (___CSS_LOADER_EXPORT___);\n\n\n//# sourceURL=webpack://jc-web-im/./src/styles/base.css?./node_modules/css-loader/dist/cjs.js??clonedRuleSet-14.use%5B1%5D!./node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-14.use%5B2%5D\n}"); + +/***/ }), + +/***/ "./src/App.vue": +/*!*********************!*\ + !*** ./src/App.vue ***! + \*********************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _App_vue_vue_type_template_id_7ba5bd90__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./App.vue?vue&type=template&id=7ba5bd90 */ \"./src/App.vue?vue&type=template&id=7ba5bd90\");\n/* harmony import */ var _App_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./App.vue?vue&type=script&setup=true&lang=js */ \"./src/App.vue?vue&type=script&setup=true&lang=js\");\n/* harmony import */ var _node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../node_modules/vue-loader/dist/exportHelper.js */ \"./node_modules/vue-loader/dist/exportHelper.js\");\n\n\n\n\n;\nconst __exports__ = /*#__PURE__*/(0,_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_App_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"], [['render',_App_vue_vue_type_template_id_7ba5bd90__WEBPACK_IMPORTED_MODULE_0__.render],['__file',\"src/App.vue\"]])\n/* hot reload */\nif (false) // removed by dead control flow\n{}\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (__exports__);\n\n//# sourceURL=webpack://jc-web-im/./src/App.vue?\n}"); + +/***/ }), + +/***/ "./src/components/ChatShell.vue": +/*!**************************************!*\ + !*** ./src/components/ChatShell.vue ***! + \**************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _ChatShell_vue_vue_type_template_id_2fe58db8__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./ChatShell.vue?vue&type=template&id=2fe58db8 */ \"./src/components/ChatShell.vue?vue&type=template&id=2fe58db8\");\n/* harmony import */ var _ChatShell_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./ChatShell.vue?vue&type=script&setup=true&lang=js */ \"./src/components/ChatShell.vue?vue&type=script&setup=true&lang=js\");\n/* harmony import */ var _node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../node_modules/vue-loader/dist/exportHelper.js */ \"./node_modules/vue-loader/dist/exportHelper.js\");\n\n\n\n\n;\nconst __exports__ = /*#__PURE__*/(0,_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ChatShell_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"], [['render',_ChatShell_vue_vue_type_template_id_2fe58db8__WEBPACK_IMPORTED_MODULE_0__.render],['__file',\"src/components/ChatShell.vue\"]])\n/* hot reload */\nif (false) // removed by dead control flow\n{}\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (__exports__);\n\n//# sourceURL=webpack://jc-web-im/./src/components/ChatShell.vue?\n}"); + +/***/ }), + +/***/ "./src/components/LoginView.vue": +/*!**************************************!*\ + !*** ./src/components/LoginView.vue ***! + \**************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _LoginView_vue_vue_type_template_id_37959aa4__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./LoginView.vue?vue&type=template&id=37959aa4 */ \"./src/components/LoginView.vue?vue&type=template&id=37959aa4\");\n/* harmony import */ var _LoginView_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./LoginView.vue?vue&type=script&setup=true&lang=js */ \"./src/components/LoginView.vue?vue&type=script&setup=true&lang=js\");\n/* harmony import */ var _node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../node_modules/vue-loader/dist/exportHelper.js */ \"./node_modules/vue-loader/dist/exportHelper.js\");\n\n\n\n\n;\nconst __exports__ = /*#__PURE__*/(0,_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_LoginView_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"], [['render',_LoginView_vue_vue_type_template_id_37959aa4__WEBPACK_IMPORTED_MODULE_0__.render],['__file',\"src/components/LoginView.vue\"]])\n/* hot reload */\nif (false) // removed by dead control flow\n{}\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (__exports__);\n\n//# sourceURL=webpack://jc-web-im/./src/components/LoginView.vue?\n}"); + +/***/ }), + +/***/ "./src/components/chat/ChatPanel.vue": +/*!*******************************************!*\ + !*** ./src/components/chat/ChatPanel.vue ***! + \*******************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _ChatPanel_vue_vue_type_template_id_358a1705__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./ChatPanel.vue?vue&type=template&id=358a1705 */ \"./src/components/chat/ChatPanel.vue?vue&type=template&id=358a1705\");\n/* harmony import */ var _ChatPanel_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./ChatPanel.vue?vue&type=script&setup=true&lang=js */ \"./src/components/chat/ChatPanel.vue?vue&type=script&setup=true&lang=js\");\n/* harmony import */ var _node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../node_modules/vue-loader/dist/exportHelper.js */ \"./node_modules/vue-loader/dist/exportHelper.js\");\n\n\n\n\n;\nconst __exports__ = /*#__PURE__*/(0,_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ChatPanel_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"], [['render',_ChatPanel_vue_vue_type_template_id_358a1705__WEBPACK_IMPORTED_MODULE_0__.render],['__file',\"src/components/chat/ChatPanel.vue\"]])\n/* hot reload */\nif (false) // removed by dead control flow\n{}\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (__exports__);\n\n//# sourceURL=webpack://jc-web-im/./src/components/chat/ChatPanel.vue?\n}"); + +/***/ }), + +/***/ "./src/components/chat/MessageComposer.vue": +/*!*************************************************!*\ + !*** ./src/components/chat/MessageComposer.vue ***! + \*************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _MessageComposer_vue_vue_type_template_id_08eca580__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./MessageComposer.vue?vue&type=template&id=08eca580 */ \"./src/components/chat/MessageComposer.vue?vue&type=template&id=08eca580\");\n/* harmony import */ var _MessageComposer_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./MessageComposer.vue?vue&type=script&setup=true&lang=js */ \"./src/components/chat/MessageComposer.vue?vue&type=script&setup=true&lang=js\");\n/* harmony import */ var _node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../node_modules/vue-loader/dist/exportHelper.js */ \"./node_modules/vue-loader/dist/exportHelper.js\");\n\n\n\n\n;\nconst __exports__ = /*#__PURE__*/(0,_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_MessageComposer_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"], [['render',_MessageComposer_vue_vue_type_template_id_08eca580__WEBPACK_IMPORTED_MODULE_0__.render],['__file',\"src/components/chat/MessageComposer.vue\"]])\n/* hot reload */\nif (false) // removed by dead control flow\n{}\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (__exports__);\n\n//# sourceURL=webpack://jc-web-im/./src/components/chat/MessageComposer.vue?\n}"); + +/***/ }), + +/***/ "./src/components/chat/MessageList.vue": +/*!*********************************************!*\ + !*** ./src/components/chat/MessageList.vue ***! + \*********************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _MessageList_vue_vue_type_template_id_a39e6c04__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./MessageList.vue?vue&type=template&id=a39e6c04 */ \"./src/components/chat/MessageList.vue?vue&type=template&id=a39e6c04\");\n/* harmony import */ var _MessageList_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./MessageList.vue?vue&type=script&setup=true&lang=js */ \"./src/components/chat/MessageList.vue?vue&type=script&setup=true&lang=js\");\n/* harmony import */ var _node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../node_modules/vue-loader/dist/exportHelper.js */ \"./node_modules/vue-loader/dist/exportHelper.js\");\n\n\n\n\n;\nconst __exports__ = /*#__PURE__*/(0,_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_MessageList_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"], [['render',_MessageList_vue_vue_type_template_id_a39e6c04__WEBPACK_IMPORTED_MODULE_0__.render],['__file',\"src/components/chat/MessageList.vue\"]])\n/* hot reload */\nif (false) // removed by dead control flow\n{}\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (__exports__);\n\n//# sourceURL=webpack://jc-web-im/./src/components/chat/MessageList.vue?\n}"); + +/***/ }), + +/***/ "./src/components/contacts/ContactPanel.vue": +/*!**************************************************!*\ + !*** ./src/components/contacts/ContactPanel.vue ***! + \**************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _ContactPanel_vue_vue_type_template_id_63d88b80__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./ContactPanel.vue?vue&type=template&id=63d88b80 */ \"./src/components/contacts/ContactPanel.vue?vue&type=template&id=63d88b80\");\n/* harmony import */ var _ContactPanel_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./ContactPanel.vue?vue&type=script&setup=true&lang=js */ \"./src/components/contacts/ContactPanel.vue?vue&type=script&setup=true&lang=js\");\n/* harmony import */ var _node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../node_modules/vue-loader/dist/exportHelper.js */ \"./node_modules/vue-loader/dist/exportHelper.js\");\n\n\n\n\n;\nconst __exports__ = /*#__PURE__*/(0,_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ContactPanel_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"], [['render',_ContactPanel_vue_vue_type_template_id_63d88b80__WEBPACK_IMPORTED_MODULE_0__.render],['__file',\"src/components/contacts/ContactPanel.vue\"]])\n/* hot reload */\nif (false) // removed by dead control flow\n{}\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (__exports__);\n\n//# sourceURL=webpack://jc-web-im/./src/components/contacts/ContactPanel.vue?\n}"); + +/***/ }), + +/***/ "./src/components/favorites/FavoriteList.vue": +/*!***************************************************!*\ + !*** ./src/components/favorites/FavoriteList.vue ***! + \***************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _FavoriteList_vue_vue_type_template_id_0086db7c__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./FavoriteList.vue?vue&type=template&id=0086db7c */ \"./src/components/favorites/FavoriteList.vue?vue&type=template&id=0086db7c\");\n/* harmony import */ var _FavoriteList_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./FavoriteList.vue?vue&type=script&setup=true&lang=js */ \"./src/components/favorites/FavoriteList.vue?vue&type=script&setup=true&lang=js\");\n/* harmony import */ var _node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../node_modules/vue-loader/dist/exportHelper.js */ \"./node_modules/vue-loader/dist/exportHelper.js\");\n\n\n\n\n;\nconst __exports__ = /*#__PURE__*/(0,_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_FavoriteList_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"], [['render',_FavoriteList_vue_vue_type_template_id_0086db7c__WEBPACK_IMPORTED_MODULE_0__.render],['__file',\"src/components/favorites/FavoriteList.vue\"]])\n/* hot reload */\nif (false) // removed by dead control flow\n{}\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (__exports__);\n\n//# sourceURL=webpack://jc-web-im/./src/components/favorites/FavoriteList.vue?\n}"); + +/***/ }), + +/***/ "./src/components/favorites/FavoritePanel.vue": +/*!****************************************************!*\ + !*** ./src/components/favorites/FavoritePanel.vue ***! + \****************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _FavoritePanel_vue_vue_type_template_id_29e1a3e0__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./FavoritePanel.vue?vue&type=template&id=29e1a3e0 */ \"./src/components/favorites/FavoritePanel.vue?vue&type=template&id=29e1a3e0\");\n/* harmony import */ var _FavoritePanel_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./FavoritePanel.vue?vue&type=script&setup=true&lang=js */ \"./src/components/favorites/FavoritePanel.vue?vue&type=script&setup=true&lang=js\");\n/* harmony import */ var _node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../node_modules/vue-loader/dist/exportHelper.js */ \"./node_modules/vue-loader/dist/exportHelper.js\");\n\n\n\n\n;\nconst __exports__ = /*#__PURE__*/(0,_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_FavoritePanel_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"], [['render',_FavoritePanel_vue_vue_type_template_id_29e1a3e0__WEBPACK_IMPORTED_MODULE_0__.render],['__file',\"src/components/favorites/FavoritePanel.vue\"]])\n/* hot reload */\nif (false) // removed by dead control flow\n{}\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (__exports__);\n\n//# sourceURL=webpack://jc-web-im/./src/components/favorites/FavoritePanel.vue?\n}"); + +/***/ }), + +/***/ "./src/components/sidebar/ContactList.vue": +/*!************************************************!*\ + !*** ./src/components/sidebar/ContactList.vue ***! + \************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _ContactList_vue_vue_type_template_id_7aaf5f2b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./ContactList.vue?vue&type=template&id=7aaf5f2b */ \"./src/components/sidebar/ContactList.vue?vue&type=template&id=7aaf5f2b\");\n/* harmony import */ var _ContactList_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./ContactList.vue?vue&type=script&setup=true&lang=js */ \"./src/components/sidebar/ContactList.vue?vue&type=script&setup=true&lang=js\");\n/* harmony import */ var _node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../node_modules/vue-loader/dist/exportHelper.js */ \"./node_modules/vue-loader/dist/exportHelper.js\");\n\n\n\n\n;\nconst __exports__ = /*#__PURE__*/(0,_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ContactList_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"], [['render',_ContactList_vue_vue_type_template_id_7aaf5f2b__WEBPACK_IMPORTED_MODULE_0__.render],['__file',\"src/components/sidebar/ContactList.vue\"]])\n/* hot reload */\nif (false) // removed by dead control flow\n{}\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (__exports__);\n\n//# sourceURL=webpack://jc-web-im/./src/components/sidebar/ContactList.vue?\n}"); + +/***/ }), + +/***/ "./src/components/sidebar/ConversationList.vue": +/*!*****************************************************!*\ + !*** ./src/components/sidebar/ConversationList.vue ***! + \*****************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _ConversationList_vue_vue_type_template_id_7a656f78__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./ConversationList.vue?vue&type=template&id=7a656f78 */ \"./src/components/sidebar/ConversationList.vue?vue&type=template&id=7a656f78\");\n/* harmony import */ var _ConversationList_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./ConversationList.vue?vue&type=script&setup=true&lang=js */ \"./src/components/sidebar/ConversationList.vue?vue&type=script&setup=true&lang=js\");\n/* harmony import */ var _node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../node_modules/vue-loader/dist/exportHelper.js */ \"./node_modules/vue-loader/dist/exportHelper.js\");\n\n\n\n\n;\nconst __exports__ = /*#__PURE__*/(0,_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_ConversationList_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"], [['render',_ConversationList_vue_vue_type_template_id_7a656f78__WEBPACK_IMPORTED_MODULE_0__.render],['__file',\"src/components/sidebar/ConversationList.vue\"]])\n/* hot reload */\nif (false) // removed by dead control flow\n{}\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (__exports__);\n\n//# sourceURL=webpack://jc-web-im/./src/components/sidebar/ConversationList.vue?\n}"); + +/***/ }), + +/***/ "./src/components/sidebar/NavigationRail.vue": +/*!***************************************************!*\ + !*** ./src/components/sidebar/NavigationRail.vue ***! + \***************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _NavigationRail_vue_vue_type_template_id_021016ee__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./NavigationRail.vue?vue&type=template&id=021016ee */ \"./src/components/sidebar/NavigationRail.vue?vue&type=template&id=021016ee\");\n/* harmony import */ var _NavigationRail_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./NavigationRail.vue?vue&type=script&setup=true&lang=js */ \"./src/components/sidebar/NavigationRail.vue?vue&type=script&setup=true&lang=js\");\n/* harmony import */ var _node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../../../node_modules/vue-loader/dist/exportHelper.js */ \"./node_modules/vue-loader/dist/exportHelper.js\");\n\n\n\n\n;\nconst __exports__ = /*#__PURE__*/(0,_node_modules_vue_loader_dist_exportHelper_js__WEBPACK_IMPORTED_MODULE_2__[\"default\"])(_NavigationRail_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"], [['render',_NavigationRail_vue_vue_type_template_id_021016ee__WEBPACK_IMPORTED_MODULE_0__.render],['__file',\"src/components/sidebar/NavigationRail.vue\"]])\n/* hot reload */\nif (false) // removed by dead control flow\n{}\n\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (__exports__);\n\n//# sourceURL=webpack://jc-web-im/./src/components/sidebar/NavigationRail.vue?\n}"); + +/***/ }), + +/***/ "./src/App.vue?vue&type=script&setup=true&lang=js": +/*!********************************************************!*\ + !*** ./src/App.vue?vue&type=script&setup=true&lang=js ***! + \********************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_App_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_App_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./App.vue?vue&type=script&setup=true&lang=js */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/App.vue?vue&type=script&setup=true&lang=js\");\n \n\n//# sourceURL=webpack://jc-web-im/./src/App.vue?\n}"); + +/***/ }), + +/***/ "./src/components/ChatShell.vue?vue&type=script&setup=true&lang=js": +/*!*************************************************************************!*\ + !*** ./src/components/ChatShell.vue?vue&type=script&setup=true&lang=js ***! + \*************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ChatShell_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ChatShell_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./ChatShell.vue?vue&type=script&setup=true&lang=js */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/ChatShell.vue?vue&type=script&setup=true&lang=js\");\n \n\n//# sourceURL=webpack://jc-web-im/./src/components/ChatShell.vue?\n}"); + +/***/ }), + +/***/ "./src/components/LoginView.vue?vue&type=script&setup=true&lang=js": +/*!*************************************************************************!*\ + !*** ./src/components/LoginView.vue?vue&type=script&setup=true&lang=js ***! + \*************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_LoginView_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_LoginView_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./LoginView.vue?vue&type=script&setup=true&lang=js */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/LoginView.vue?vue&type=script&setup=true&lang=js\");\n \n\n//# sourceURL=webpack://jc-web-im/./src/components/LoginView.vue?\n}"); + +/***/ }), + +/***/ "./src/components/chat/ChatPanel.vue?vue&type=script&setup=true&lang=js": +/*!******************************************************************************!*\ + !*** ./src/components/chat/ChatPanel.vue?vue&type=script&setup=true&lang=js ***! + \******************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ChatPanel_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ChatPanel_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./ChatPanel.vue?vue&type=script&setup=true&lang=js */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/ChatPanel.vue?vue&type=script&setup=true&lang=js\");\n \n\n//# sourceURL=webpack://jc-web-im/./src/components/chat/ChatPanel.vue?\n}"); + +/***/ }), + +/***/ "./src/components/chat/MessageComposer.vue?vue&type=script&setup=true&lang=js": +/*!************************************************************************************!*\ + !*** ./src/components/chat/MessageComposer.vue?vue&type=script&setup=true&lang=js ***! + \************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_MessageComposer_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_MessageComposer_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./MessageComposer.vue?vue&type=script&setup=true&lang=js */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/MessageComposer.vue?vue&type=script&setup=true&lang=js\");\n \n\n//# sourceURL=webpack://jc-web-im/./src/components/chat/MessageComposer.vue?\n}"); + +/***/ }), + +/***/ "./src/components/chat/MessageList.vue?vue&type=script&setup=true&lang=js": +/*!********************************************************************************!*\ + !*** ./src/components/chat/MessageList.vue?vue&type=script&setup=true&lang=js ***! + \********************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_MessageList_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_MessageList_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./MessageList.vue?vue&type=script&setup=true&lang=js */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/MessageList.vue?vue&type=script&setup=true&lang=js\");\n \n\n//# sourceURL=webpack://jc-web-im/./src/components/chat/MessageList.vue?\n}"); + +/***/ }), + +/***/ "./src/components/contacts/ContactPanel.vue?vue&type=script&setup=true&lang=js": +/*!*************************************************************************************!*\ + !*** ./src/components/contacts/ContactPanel.vue?vue&type=script&setup=true&lang=js ***! + \*************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ContactPanel_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ContactPanel_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./ContactPanel.vue?vue&type=script&setup=true&lang=js */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/contacts/ContactPanel.vue?vue&type=script&setup=true&lang=js\");\n \n\n//# sourceURL=webpack://jc-web-im/./src/components/contacts/ContactPanel.vue?\n}"); + +/***/ }), + +/***/ "./src/components/favorites/FavoriteList.vue?vue&type=script&setup=true&lang=js": +/*!**************************************************************************************!*\ + !*** ./src/components/favorites/FavoriteList.vue?vue&type=script&setup=true&lang=js ***! + \**************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_FavoriteList_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_FavoriteList_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./FavoriteList.vue?vue&type=script&setup=true&lang=js */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/favorites/FavoriteList.vue?vue&type=script&setup=true&lang=js\");\n \n\n//# sourceURL=webpack://jc-web-im/./src/components/favorites/FavoriteList.vue?\n}"); + +/***/ }), + +/***/ "./src/components/favorites/FavoritePanel.vue?vue&type=script&setup=true&lang=js": +/*!***************************************************************************************!*\ + !*** ./src/components/favorites/FavoritePanel.vue?vue&type=script&setup=true&lang=js ***! + \***************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_FavoritePanel_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_FavoritePanel_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./FavoritePanel.vue?vue&type=script&setup=true&lang=js */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/favorites/FavoritePanel.vue?vue&type=script&setup=true&lang=js\");\n \n\n//# sourceURL=webpack://jc-web-im/./src/components/favorites/FavoritePanel.vue?\n}"); + +/***/ }), + +/***/ "./src/components/sidebar/ContactList.vue?vue&type=script&setup=true&lang=js": +/*!***********************************************************************************!*\ + !*** ./src/components/sidebar/ContactList.vue?vue&type=script&setup=true&lang=js ***! + \***********************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ContactList_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ContactList_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./ContactList.vue?vue&type=script&setup=true&lang=js */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/ContactList.vue?vue&type=script&setup=true&lang=js\");\n \n\n//# sourceURL=webpack://jc-web-im/./src/components/sidebar/ContactList.vue?\n}"); + +/***/ }), + +/***/ "./src/components/sidebar/ConversationList.vue?vue&type=script&setup=true&lang=js": +/*!****************************************************************************************!*\ + !*** ./src/components/sidebar/ConversationList.vue?vue&type=script&setup=true&lang=js ***! + \****************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ConversationList_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ConversationList_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./ConversationList.vue?vue&type=script&setup=true&lang=js */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/ConversationList.vue?vue&type=script&setup=true&lang=js\");\n \n\n//# sourceURL=webpack://jc-web-im/./src/components/sidebar/ConversationList.vue?\n}"); + +/***/ }), + +/***/ "./src/components/sidebar/NavigationRail.vue?vue&type=script&setup=true&lang=js": +/*!**************************************************************************************!*\ + !*** ./src/components/sidebar/NavigationRail.vue?vue&type=script&setup=true&lang=js ***! + \**************************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_NavigationRail_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_NavigationRail_vue_vue_type_script_setup_true_lang_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./NavigationRail.vue?vue&type=script&setup=true&lang=js */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/NavigationRail.vue?vue&type=script&setup=true&lang=js\");\n \n\n//# sourceURL=webpack://jc-web-im/./src/components/sidebar/NavigationRail.vue?\n}"); + +/***/ }), + +/***/ "./src/App.vue?vue&type=template&id=7ba5bd90": +/*!***************************************************!*\ + !*** ./src/App.vue?vue&type=template&id=7ba5bd90 ***! + \***************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_App_vue_vue_type_template_id_7ba5bd90__WEBPACK_IMPORTED_MODULE_0__.render; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_App_vue_vue_type_template_id_7ba5bd90__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./App.vue?vue&type=template&id=7ba5bd90 */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/App.vue?vue&type=template&id=7ba5bd90\");\n\n\n//# sourceURL=webpack://jc-web-im/./src/App.vue?\n}"); + +/***/ }), + +/***/ "./src/components/ChatShell.vue?vue&type=template&id=2fe58db8": +/*!********************************************************************!*\ + !*** ./src/components/ChatShell.vue?vue&type=template&id=2fe58db8 ***! + \********************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ChatShell_vue_vue_type_template_id_2fe58db8__WEBPACK_IMPORTED_MODULE_0__.render; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ChatShell_vue_vue_type_template_id_2fe58db8__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./ChatShell.vue?vue&type=template&id=2fe58db8 */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/ChatShell.vue?vue&type=template&id=2fe58db8\");\n\n\n//# sourceURL=webpack://jc-web-im/./src/components/ChatShell.vue?\n}"); + +/***/ }), + +/***/ "./src/components/LoginView.vue?vue&type=template&id=37959aa4": +/*!********************************************************************!*\ + !*** ./src/components/LoginView.vue?vue&type=template&id=37959aa4 ***! + \********************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_LoginView_vue_vue_type_template_id_37959aa4__WEBPACK_IMPORTED_MODULE_0__.render; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_LoginView_vue_vue_type_template_id_37959aa4__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./LoginView.vue?vue&type=template&id=37959aa4 */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/LoginView.vue?vue&type=template&id=37959aa4\");\n\n\n//# sourceURL=webpack://jc-web-im/./src/components/LoginView.vue?\n}"); + +/***/ }), + +/***/ "./src/components/chat/ChatPanel.vue?vue&type=template&id=358a1705": +/*!*************************************************************************!*\ + !*** ./src/components/chat/ChatPanel.vue?vue&type=template&id=358a1705 ***! + \*************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ChatPanel_vue_vue_type_template_id_358a1705__WEBPACK_IMPORTED_MODULE_0__.render; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ChatPanel_vue_vue_type_template_id_358a1705__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./ChatPanel.vue?vue&type=template&id=358a1705 */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/ChatPanel.vue?vue&type=template&id=358a1705\");\n\n\n//# sourceURL=webpack://jc-web-im/./src/components/chat/ChatPanel.vue?\n}"); + +/***/ }), + +/***/ "./src/components/chat/MessageComposer.vue?vue&type=template&id=08eca580": +/*!*******************************************************************************!*\ + !*** ./src/components/chat/MessageComposer.vue?vue&type=template&id=08eca580 ***! + \*******************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_MessageComposer_vue_vue_type_template_id_08eca580__WEBPACK_IMPORTED_MODULE_0__.render; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_MessageComposer_vue_vue_type_template_id_08eca580__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./MessageComposer.vue?vue&type=template&id=08eca580 */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/MessageComposer.vue?vue&type=template&id=08eca580\");\n\n\n//# sourceURL=webpack://jc-web-im/./src/components/chat/MessageComposer.vue?\n}"); + +/***/ }), + +/***/ "./src/components/chat/MessageList.vue?vue&type=template&id=a39e6c04": +/*!***************************************************************************!*\ + !*** ./src/components/chat/MessageList.vue?vue&type=template&id=a39e6c04 ***! + \***************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_MessageList_vue_vue_type_template_id_a39e6c04__WEBPACK_IMPORTED_MODULE_0__.render; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_MessageList_vue_vue_type_template_id_a39e6c04__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./MessageList.vue?vue&type=template&id=a39e6c04 */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/chat/MessageList.vue?vue&type=template&id=a39e6c04\");\n\n\n//# sourceURL=webpack://jc-web-im/./src/components/chat/MessageList.vue?\n}"); + +/***/ }), + +/***/ "./src/components/contacts/ContactPanel.vue?vue&type=template&id=63d88b80": +/*!********************************************************************************!*\ + !*** ./src/components/contacts/ContactPanel.vue?vue&type=template&id=63d88b80 ***! + \********************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ContactPanel_vue_vue_type_template_id_63d88b80__WEBPACK_IMPORTED_MODULE_0__.render; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ContactPanel_vue_vue_type_template_id_63d88b80__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./ContactPanel.vue?vue&type=template&id=63d88b80 */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/contacts/ContactPanel.vue?vue&type=template&id=63d88b80\");\n\n\n//# sourceURL=webpack://jc-web-im/./src/components/contacts/ContactPanel.vue?\n}"); + +/***/ }), + +/***/ "./src/components/favorites/FavoriteList.vue?vue&type=template&id=0086db7c": +/*!*********************************************************************************!*\ + !*** ./src/components/favorites/FavoriteList.vue?vue&type=template&id=0086db7c ***! + \*********************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_FavoriteList_vue_vue_type_template_id_0086db7c__WEBPACK_IMPORTED_MODULE_0__.render; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_FavoriteList_vue_vue_type_template_id_0086db7c__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./FavoriteList.vue?vue&type=template&id=0086db7c */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/favorites/FavoriteList.vue?vue&type=template&id=0086db7c\");\n\n\n//# sourceURL=webpack://jc-web-im/./src/components/favorites/FavoriteList.vue?\n}"); + +/***/ }), + +/***/ "./src/components/favorites/FavoritePanel.vue?vue&type=template&id=29e1a3e0": +/*!**********************************************************************************!*\ + !*** ./src/components/favorites/FavoritePanel.vue?vue&type=template&id=29e1a3e0 ***! + \**********************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_FavoritePanel_vue_vue_type_template_id_29e1a3e0__WEBPACK_IMPORTED_MODULE_0__.render; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_FavoritePanel_vue_vue_type_template_id_29e1a3e0__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./FavoritePanel.vue?vue&type=template&id=29e1a3e0 */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/favorites/FavoritePanel.vue?vue&type=template&id=29e1a3e0\");\n\n\n//# sourceURL=webpack://jc-web-im/./src/components/favorites/FavoritePanel.vue?\n}"); + +/***/ }), + +/***/ "./src/components/sidebar/ContactList.vue?vue&type=template&id=7aaf5f2b": +/*!******************************************************************************!*\ + !*** ./src/components/sidebar/ContactList.vue?vue&type=template&id=7aaf5f2b ***! + \******************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ContactList_vue_vue_type_template_id_7aaf5f2b__WEBPACK_IMPORTED_MODULE_0__.render; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ContactList_vue_vue_type_template_id_7aaf5f2b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./ContactList.vue?vue&type=template&id=7aaf5f2b */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/ContactList.vue?vue&type=template&id=7aaf5f2b\");\n\n\n//# sourceURL=webpack://jc-web-im/./src/components/sidebar/ContactList.vue?\n}"); + +/***/ }), + +/***/ "./src/components/sidebar/ConversationList.vue?vue&type=template&id=7a656f78": +/*!***********************************************************************************!*\ + !*** ./src/components/sidebar/ConversationList.vue?vue&type=template&id=7a656f78 ***! + \***********************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ConversationList_vue_vue_type_template_id_7a656f78__WEBPACK_IMPORTED_MODULE_0__.render; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_ConversationList_vue_vue_type_template_id_7a656f78__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./ConversationList.vue?vue&type=template&id=7a656f78 */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/ConversationList.vue?vue&type=template&id=7a656f78\");\n\n\n//# sourceURL=webpack://jc-web-im/./src/components/sidebar/ConversationList.vue?\n}"); + +/***/ }), + +/***/ "./src/components/sidebar/NavigationRail.vue?vue&type=template&id=021016ee": +/*!*********************************************************************************!*\ + !*** ./src/components/sidebar/NavigationRail.vue?vue&type=template&id=021016ee ***! + \*********************************************************************************/ +/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) { + +"use strict"; +eval("{__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ render: function() { return /* reexport safe */ _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_NavigationRail_vue_vue_type_template_id_021016ee__WEBPACK_IMPORTED_MODULE_0__.render; }\n/* harmony export */ });\n/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_40_use_0_node_modules_vue_loader_dist_templateLoader_js_ruleSet_1_rules_3_node_modules_vue_loader_dist_index_js_ruleSet_0_use_0_NavigationRail_vue_vue_type_template_id_021016ee__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!../../../node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!../../../node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./NavigationRail.vue?vue&type=template&id=021016ee */ \"./node_modules/babel-loader/lib/index.js??clonedRuleSet-40.use[0]!./node_modules/vue-loader/dist/templateLoader.js??ruleSet[1].rules[3]!./node_modules/vue-loader/dist/index.js??ruleSet[0].use[0]!./src/components/sidebar/NavigationRail.vue?vue&type=template&id=021016ee\");\n\n\n//# sourceURL=webpack://jc-web-im/./src/components/sidebar/NavigationRail.vue?\n}"); + +/***/ }), + +/***/ "./src/styles/base.css": +/*!*****************************!*\ + !*** ./src/styles/base.css ***! + \*****************************/ +/***/ (function(module, __unused_webpack_exports, __webpack_require__) { + +eval("{// style-loader: Adds some css to the DOM by adding a