most-box 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/admin/index.html +0 -0
- package/out/app/index.html +0 -0
- package/out/assets/{FilePreviewOverlay-D8H61qbp.js → FilePreviewOverlay-CM5IqnZD.js} +1 -1
- package/out/assets/{MarketingHeader-CrYsQsQ5.js → MarketingHeader-B45AaM29.js} +1 -1
- package/out/assets/{MarketingLayout-CQurMOSD.js → MarketingLayout-A00pEOa2.js} +1 -1
- package/out/assets/{MilkdownEditor-DrsyaR4h.js → MilkdownEditor-BTq9bDAf.js} +1 -1
- package/out/assets/{OpenSidebarButton-DnEgqP0S.js → OpenSidebarButton-D8qfUGzU.js} +1 -1
- package/out/assets/{SidebarAccount-B4OAl0as.js → SidebarAccount-_RBE5K_o.js} +1 -1
- package/out/assets/{SidebarHomeLink-BdsUM4Kt.js → SidebarHomeLink-qMFEGjZt.js} +1 -1
- package/out/assets/{downloadValidation-DbVOwdmC.js → downloadValidation-CSXwoQKW.js} +1 -1
- package/out/assets/index-BHZhApbo.css +1 -0
- package/out/assets/index-BRhsWNj1.css +1 -0
- package/out/assets/{index-BZ-f0CZd.js → index-C-_ZwxuN.js} +2 -2
- package/out/assets/index-CYVRmppz.css +1 -0
- package/out/assets/index.lazy-0YYV0CR3.js +1 -0
- package/out/assets/index.lazy-BTYGr9Vb.js +1 -0
- package/out/assets/{index.lazy-B7AbDiMU.js → index.lazy-BVAtQtTa.js} +1 -1
- package/out/assets/{index.lazy-oFaoa4qT.js → index.lazy-BbdLA2L-.js} +1 -1
- package/out/assets/{index.lazy-DVeqmqaA.js → index.lazy-CbiLtziY.js} +1 -1
- package/out/assets/{index.lazy-CvKEcWlH.js → index.lazy-CcEdBg3i.js} +2 -2
- package/out/assets/index.lazy-CePHsLXG.js +1 -0
- package/out/assets/{index.lazy-rfgjFZNH.js → index.lazy-DG_3_VdF.js} +1 -1
- package/out/assets/{index.lazy-zpQXhgmn.js → index.lazy-DR0rWEWZ.js} +1 -1
- package/out/assets/{index.lazy-hF807bh0.js → index.lazy-DSyTSCJD.js} +1 -1
- package/out/assets/{index.lazy-DB9Ht9LJ.js → index.lazy-DUhsuPLv.js} +1 -1
- package/out/assets/{index.lazy-BsciBHf_.js → index.lazy-DVnhWbzH.js} +1 -1
- package/out/assets/{index.lazy-CgKPe02l.js → index.lazy-DZpkg1DJ.js} +1 -1
- package/out/assets/useChannelMessages-DOaWHdAP.js +4 -0
- package/out/assets/{useGameRoom-CRJL0kcm.js → useGameRoom-hHkV65ii.js} +1 -1
- package/out/assets/{userProfile-DBkH4s29.js → userProfile-DwZip0Dt.js} +1 -1
- package/out/chat/index.html +0 -0
- package/out/chat/join/index.html +0 -0
- package/out/download/index.html +2 -2
- package/out/game/gandengyan/index.html +0 -0
- package/out/game/index.html +0 -0
- package/out/game/zhajinhua/index.html +0 -0
- package/out/index.html +2 -2
- package/out/note/index.html +0 -0
- package/out/ping/index.html +2 -2
- package/out/profile/index.html +0 -0
- package/out/web3/index.html +0 -0
- package/package.json +1 -1
- package/server/index.js +59 -2
- package/server/src/http/app.js +7 -1
- package/server/src/http/nodeStatus.js +6 -0
- package/server/src/http/routes/channelRoutes.js +12 -0
- package/server/src/index.js +510 -5
- package/out/assets/index-3OD3Chi9.css +0 -1
- package/out/assets/index-BSOvFG3o.css +0 -1
- package/out/assets/index-Cde4g_dk.css +0 -1
- package/out/assets/index.lazy-BxoGfWEW.js +0 -1
- package/out/assets/index.lazy-CKbrHko5.js +0 -1
- package/out/assets/index.lazy-Citucq1G.js +0 -1
- package/out/assets/useChannelMessages-B6RMaNDH.js +0 -3
package/server/src/index.js
CHANGED
|
@@ -93,6 +93,8 @@ import {
|
|
|
93
93
|
} from './config.js'
|
|
94
94
|
|
|
95
95
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
96
|
+
const CHANNEL_PRESENCE_HEARTBEAT_MS = 15 * 1000
|
|
97
|
+
const CHANNEL_PRESENCE_TIMEOUT_MS = 45 * 1000
|
|
96
98
|
const CHANNEL_WELCOME_MESSAGE = '我加入了群聊'
|
|
97
99
|
|
|
98
100
|
function createMemoryDuplexPair() {
|
|
@@ -158,6 +160,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
158
160
|
#channelPeers = new Map()
|
|
159
161
|
#channelCandidateCache = new Map()
|
|
160
162
|
#channelStreams = new Set()
|
|
163
|
+
#channelPresenceSessions = new Map()
|
|
164
|
+
#channelPresenceProfiles = new Map()
|
|
165
|
+
#channelPresenceSweepTimer = null
|
|
161
166
|
|
|
162
167
|
#accountMetadata = { profiles: {} }
|
|
163
168
|
|
|
@@ -190,6 +195,90 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
190
195
|
}
|
|
191
196
|
}
|
|
192
197
|
|
|
198
|
+
getChannelPresence(channelKeyInput, options = {}) {
|
|
199
|
+
this.#ensureInitialized()
|
|
200
|
+
this.#assertChannelMember(channelKeyInput, options.ownerAddress)
|
|
201
|
+
const channel = this.#resolveChannel(channelKeyInput, options.ownerAddress)
|
|
202
|
+
this.#pruneStaleChannelPresence()
|
|
203
|
+
return this.#getChannelPresenceList(channel.channelKey)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
joinChannelPresence(channelKeyInput, options = {}) {
|
|
207
|
+
this.#ensureInitialized()
|
|
208
|
+
this.#assertChannelMember(channelKeyInput, options.ownerAddress)
|
|
209
|
+
const channel = this.#resolveChannel(channelKeyInput, options.ownerAddress)
|
|
210
|
+
const event = this.#upsertChannelPresenceSession(channel, {
|
|
211
|
+
...options,
|
|
212
|
+
address: options.ownerAddress,
|
|
213
|
+
local: true,
|
|
214
|
+
})
|
|
215
|
+
if (event) {
|
|
216
|
+
this.#broadcastChannelPresence(event)
|
|
217
|
+
}
|
|
218
|
+
return this.#getChannelPresenceList(channel.channelKey)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
heartbeatChannelPresence(channelKeyInput, options = {}) {
|
|
222
|
+
this.#ensureInitialized()
|
|
223
|
+
this.#assertChannelMember(channelKeyInput, options.ownerAddress)
|
|
224
|
+
const channel = this.#resolveChannel(channelKeyInput, options.ownerAddress)
|
|
225
|
+
const event = this.#touchChannelPresenceSession(channel, {
|
|
226
|
+
...options,
|
|
227
|
+
address: options.ownerAddress,
|
|
228
|
+
local: true,
|
|
229
|
+
})
|
|
230
|
+
this.#broadcastChannelPresence(
|
|
231
|
+
event ||
|
|
232
|
+
this.#formatChannelPresence(
|
|
233
|
+
channel.channelKey,
|
|
234
|
+
options.ownerAddress,
|
|
235
|
+
'heartbeat'
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
return this.#getChannelPresenceList(channel.channelKey)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
updateChannelPresenceProfile(channelKeyInput, options = {}) {
|
|
242
|
+
this.#ensureInitialized()
|
|
243
|
+
this.#assertChannelMember(channelKeyInput, options.ownerAddress)
|
|
244
|
+
const channel = this.#resolveChannel(channelKeyInput, options.ownerAddress)
|
|
245
|
+
const event = this.#updateChannelPresenceProfile(channel, {
|
|
246
|
+
...options,
|
|
247
|
+
address: options.ownerAddress,
|
|
248
|
+
local: true,
|
|
249
|
+
})
|
|
250
|
+
if (event) {
|
|
251
|
+
this.#broadcastChannelPresence(event)
|
|
252
|
+
}
|
|
253
|
+
return this.#getChannelPresenceList(channel.channelKey)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
leaveChannelPresence(channelKeyInput, options = {}) {
|
|
257
|
+
this.#ensureInitialized()
|
|
258
|
+
this.#assertChannelMember(channelKeyInput, options.ownerAddress)
|
|
259
|
+
const channel = this.#resolveChannel(channelKeyInput, options.ownerAddress)
|
|
260
|
+
const events = this.#removeChannelPresenceSessions(channel.channelKey, {
|
|
261
|
+
...options,
|
|
262
|
+
address: options.ownerAddress,
|
|
263
|
+
})
|
|
264
|
+
events.forEach(event => this.#broadcastChannelPresence(event))
|
|
265
|
+
return this.#getChannelPresenceList(channel.channelKey)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
clearChannelPresenceSource(sourceId, options = {}) {
|
|
269
|
+
this.#ensureInitialized()
|
|
270
|
+
const events = this.#removeChannelPresenceSessionsBySource(sourceId)
|
|
271
|
+
if (options.broadcast) {
|
|
272
|
+
events.forEach(event => this.#broadcastChannelPresence(event))
|
|
273
|
+
}
|
|
274
|
+
return events
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
pruneChannelPresence() {
|
|
278
|
+
this.#ensureInitialized()
|
|
279
|
+
return this.#pruneStaleChannelPresence()
|
|
280
|
+
}
|
|
281
|
+
|
|
193
282
|
/**
|
|
194
283
|
* 初始化引擎 — 必须在调用其他方法之前调用
|
|
195
284
|
*/
|
|
@@ -363,6 +452,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
363
452
|
console.log(`[MostBox] Engine initialized successfully`)
|
|
364
453
|
this.emit('ready')
|
|
365
454
|
this.#resumeHoldingsInBackground()
|
|
455
|
+
this.#startChannelPresenceSweeper()
|
|
366
456
|
|
|
367
457
|
return this
|
|
368
458
|
}
|
|
@@ -419,6 +509,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
419
509
|
this.#channelPeers.clear()
|
|
420
510
|
this.#channelCandidateCache.clear()
|
|
421
511
|
this.#channelStreams.clear()
|
|
512
|
+
this.#clearChannelPresenceRuntime()
|
|
422
513
|
this.#channels = []
|
|
423
514
|
|
|
424
515
|
if (this.#store) {
|
|
@@ -2588,7 +2679,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2588
2679
|
/**
|
|
2589
2680
|
* 获取频道内在线用户
|
|
2590
2681
|
* @param {string} channelKeyInput - 内部频道 key,或本地唯一短频道 ID
|
|
2591
|
-
* @returns {Array<{ peerId: string, authorName: string, lastSeen: number }>}
|
|
2682
|
+
* @returns {Array<{ peerId: string, authorName: string, lastSeen: number, memberAddresses: string[] }>}
|
|
2592
2683
|
*/
|
|
2593
2684
|
getChannelPeers(channelKeyInput, options = {}) {
|
|
2594
2685
|
this.#ensureInitialized()
|
|
@@ -2603,6 +2694,9 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
2603
2694
|
return [...peers.values()].map(p => ({
|
|
2604
2695
|
peerId: p.peerId,
|
|
2605
2696
|
authorName: p.authorName,
|
|
2697
|
+
memberAddresses: uniqueStrings(p.memberAddresses).map(address =>
|
|
2698
|
+
normalizeOwnerAddress(address)
|
|
2699
|
+
).filter(Boolean),
|
|
2606
2700
|
lastSeen: p.lastSeen,
|
|
2607
2701
|
}))
|
|
2608
2702
|
}
|
|
@@ -3101,6 +3195,319 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
3101
3195
|
)
|
|
3102
3196
|
}
|
|
3103
3197
|
|
|
3198
|
+
#getChannelMemberAddresses(channel) {
|
|
3199
|
+
const members = Array.isArray(channel?.members) ? channel.members : []
|
|
3200
|
+
return uniqueStrings(
|
|
3201
|
+
members.map(member => normalizeOwnerAddress(member?.address))
|
|
3202
|
+
)
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
#normalizePresenceSessionId(sessionId) {
|
|
3206
|
+
return String(sessionId || 'default').trim().slice(0, 120) || 'default'
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
#normalizePresenceSourceId(options = {}) {
|
|
3210
|
+
const sourceId = String(options.sourceId || '').trim()
|
|
3211
|
+
if (sourceId) return sourceId.slice(0, 160)
|
|
3212
|
+
const sourcePeerId = String(options.sourcePeerId || '').trim()
|
|
3213
|
+
if (sourcePeerId) return `peer:${sourcePeerId}`.slice(0, 180)
|
|
3214
|
+
return 'local'
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
#getPresenceSessionKey(options = {}) {
|
|
3218
|
+
return [
|
|
3219
|
+
this.#normalizePresenceSourceId(options),
|
|
3220
|
+
normalizeOwnerAddress(options.address),
|
|
3221
|
+
this.#normalizePresenceSessionId(options.sessionId),
|
|
3222
|
+
].join(':')
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
#getChannelPresenceSessionMap(channelKey) {
|
|
3226
|
+
if (!this.#channelPresenceSessions.has(channelKey)) {
|
|
3227
|
+
this.#channelPresenceSessions.set(channelKey, new Map())
|
|
3228
|
+
}
|
|
3229
|
+
return this.#channelPresenceSessions.get(channelKey)
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
#getChannelPresenceProfileMap(channelKey) {
|
|
3233
|
+
if (!this.#channelPresenceProfiles.has(channelKey)) {
|
|
3234
|
+
this.#channelPresenceProfiles.set(channelKey, new Map())
|
|
3235
|
+
}
|
|
3236
|
+
return this.#channelPresenceProfiles.get(channelKey)
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
#isChannelPresenceAddressOnline(channelKey, address) {
|
|
3240
|
+
const normalizedAddress = normalizeOwnerAddress(address)
|
|
3241
|
+
if (!normalizedAddress) return false
|
|
3242
|
+
const sessions = this.#channelPresenceSessions.get(channelKey)
|
|
3243
|
+
if (!sessions) return false
|
|
3244
|
+
return [...sessions.values()].some(
|
|
3245
|
+
session => session.address === normalizedAddress
|
|
3246
|
+
)
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
#getChannelPresenceList(channelKey) {
|
|
3250
|
+
const sessions = this.#channelPresenceSessions.get(channelKey)
|
|
3251
|
+
if (!sessions) return []
|
|
3252
|
+
const addresses = uniqueStrings(
|
|
3253
|
+
[...sessions.values()].map(session => session.address)
|
|
3254
|
+
)
|
|
3255
|
+
return addresses
|
|
3256
|
+
.map(address => this.#formatChannelPresence(channelKey, address))
|
|
3257
|
+
.filter(Boolean)
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
#formatChannelPresence(channelKey, address, status = 'online') {
|
|
3261
|
+
const normalizedAddress = normalizeOwnerAddress(address)
|
|
3262
|
+
if (!normalizedAddress) return null
|
|
3263
|
+
const sessions = [
|
|
3264
|
+
...(this.#channelPresenceSessions.get(channelKey)?.values() || []),
|
|
3265
|
+
].filter(session => session.address === normalizedAddress)
|
|
3266
|
+
const profile = this.#channelPresenceProfiles
|
|
3267
|
+
.get(channelKey)
|
|
3268
|
+
?.get(normalizedAddress)
|
|
3269
|
+
const lastSeen = Math.max(
|
|
3270
|
+
0,
|
|
3271
|
+
Number(profile?.lastSeen) || 0,
|
|
3272
|
+
...sessions.map(session => Number(session.lastSeen) || 0)
|
|
3273
|
+
)
|
|
3274
|
+
return {
|
|
3275
|
+
channelKey,
|
|
3276
|
+
channelId:
|
|
3277
|
+
this.#channels.find(channel => channel.channelKey === channelKey)
|
|
3278
|
+
?.channelId || channelKey,
|
|
3279
|
+
address: normalizedAddress,
|
|
3280
|
+
displayName: profile?.displayName || undefined,
|
|
3281
|
+
avatar: profile?.avatar || undefined,
|
|
3282
|
+
profileUpdatedAt: profile?.profileUpdatedAt || undefined,
|
|
3283
|
+
lastSeen,
|
|
3284
|
+
online: sessions.length > 0,
|
|
3285
|
+
local: sessions.some(session => session.local),
|
|
3286
|
+
status,
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
#upsertChannelPresenceProfile(channelKey, address, options = {}, now = Date.now()) {
|
|
3291
|
+
const normalizedAddress = normalizeOwnerAddress(address)
|
|
3292
|
+
if (!normalizedAddress) return false
|
|
3293
|
+
const hasDisplayName = Object.prototype.hasOwnProperty.call(
|
|
3294
|
+
options,
|
|
3295
|
+
'displayName'
|
|
3296
|
+
)
|
|
3297
|
+
const hasAvatar = Object.prototype.hasOwnProperty.call(options, 'avatar')
|
|
3298
|
+
const profileUpdatedAt = Number(options.profileUpdatedAt)
|
|
3299
|
+
const hasProfileUpdatedAt =
|
|
3300
|
+
Number.isFinite(profileUpdatedAt) && profileUpdatedAt > 0
|
|
3301
|
+
if (!hasDisplayName && !hasAvatar && !hasProfileUpdatedAt) return false
|
|
3302
|
+
|
|
3303
|
+
const profiles = this.#getChannelPresenceProfileMap(channelKey)
|
|
3304
|
+
const previous = profiles.get(normalizedAddress)
|
|
3305
|
+
const nextUpdatedAt = hasProfileUpdatedAt
|
|
3306
|
+
? Math.floor(profileUpdatedAt)
|
|
3307
|
+
: now
|
|
3308
|
+
if (
|
|
3309
|
+
previous?.profileUpdatedAt &&
|
|
3310
|
+
hasProfileUpdatedAt &&
|
|
3311
|
+
nextUpdatedAt < previous.profileUpdatedAt
|
|
3312
|
+
) {
|
|
3313
|
+
return false
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
const next = {
|
|
3317
|
+
address: normalizedAddress,
|
|
3318
|
+
displayName: previous?.displayName || '',
|
|
3319
|
+
avatar: previous?.avatar || '',
|
|
3320
|
+
profileUpdatedAt: nextUpdatedAt,
|
|
3321
|
+
lastSeen: now,
|
|
3322
|
+
}
|
|
3323
|
+
if (hasDisplayName) {
|
|
3324
|
+
next.displayName = normalizeChannelDisplayName(
|
|
3325
|
+
options.displayName,
|
|
3326
|
+
normalizedAddress
|
|
3327
|
+
)
|
|
3328
|
+
}
|
|
3329
|
+
if (hasAvatar) {
|
|
3330
|
+
next.avatar = normalizeChannelAvatar(options.avatar)
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
const changed =
|
|
3334
|
+
!previous ||
|
|
3335
|
+
previous.displayName !== next.displayName ||
|
|
3336
|
+
previous.avatar !== next.avatar ||
|
|
3337
|
+
previous.profileUpdatedAt !== next.profileUpdatedAt
|
|
3338
|
+
profiles.set(normalizedAddress, next)
|
|
3339
|
+
return changed
|
|
3340
|
+
}
|
|
3341
|
+
|
|
3342
|
+
#emitChannelPresence(channelKey, address, status) {
|
|
3343
|
+
const event = this.#formatChannelPresence(channelKey, address, status)
|
|
3344
|
+
if (event) {
|
|
3345
|
+
this.emit('channel:presence', event)
|
|
3346
|
+
}
|
|
3347
|
+
return event
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
#upsertChannelPresenceSession(channel, options = {}) {
|
|
3351
|
+
const address = normalizeOwnerAddress(options.address)
|
|
3352
|
+
if (!address) return null
|
|
3353
|
+
const now = Number(options.lastSeen) || Date.now()
|
|
3354
|
+
const channelKey = channel.channelKey
|
|
3355
|
+
const wasOnline = this.#isChannelPresenceAddressOnline(channelKey, address)
|
|
3356
|
+
const session = {
|
|
3357
|
+
sessionId: this.#normalizePresenceSessionId(options.sessionId),
|
|
3358
|
+
sourceId: this.#normalizePresenceSourceId(options),
|
|
3359
|
+
address,
|
|
3360
|
+
channelKey,
|
|
3361
|
+
lastSeen: now,
|
|
3362
|
+
local: options.local === true,
|
|
3363
|
+
sourcePeerId: String(options.sourcePeerId || '').trim(),
|
|
3364
|
+
}
|
|
3365
|
+
this.#getChannelPresenceSessionMap(channelKey).set(
|
|
3366
|
+
this.#getPresenceSessionKey(session),
|
|
3367
|
+
session
|
|
3368
|
+
)
|
|
3369
|
+
const profileChanged = this.#upsertChannelPresenceProfile(
|
|
3370
|
+
channelKey,
|
|
3371
|
+
address,
|
|
3372
|
+
options,
|
|
3373
|
+
now
|
|
3374
|
+
)
|
|
3375
|
+
if (!wasOnline) {
|
|
3376
|
+
return this.#emitChannelPresence(channelKey, address, 'online')
|
|
3377
|
+
}
|
|
3378
|
+
if (profileChanged) {
|
|
3379
|
+
return this.#emitChannelPresence(channelKey, address, 'profile')
|
|
3380
|
+
}
|
|
3381
|
+
return null
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
#touchChannelPresenceSession(channel, options = {}) {
|
|
3385
|
+
const address = normalizeOwnerAddress(options.address)
|
|
3386
|
+
if (!address) return null
|
|
3387
|
+
const channelKey = channel.channelKey
|
|
3388
|
+
const sessionKey = this.#getPresenceSessionKey({
|
|
3389
|
+
...options,
|
|
3390
|
+
address,
|
|
3391
|
+
})
|
|
3392
|
+
const sessions = this.#getChannelPresenceSessionMap(channelKey)
|
|
3393
|
+
const existing = sessions.get(sessionKey)
|
|
3394
|
+
if (!existing) {
|
|
3395
|
+
return this.#upsertChannelPresenceSession(channel, {
|
|
3396
|
+
...options,
|
|
3397
|
+
address,
|
|
3398
|
+
})
|
|
3399
|
+
}
|
|
3400
|
+
existing.lastSeen = Number(options.lastSeen) || Date.now()
|
|
3401
|
+
sessions.set(sessionKey, existing)
|
|
3402
|
+
return null
|
|
3403
|
+
}
|
|
3404
|
+
|
|
3405
|
+
#updateChannelPresenceProfile(channel, options = {}) {
|
|
3406
|
+
const address = normalizeOwnerAddress(options.address)
|
|
3407
|
+
if (!address) return null
|
|
3408
|
+
const now = Number(options.lastSeen) || Date.now()
|
|
3409
|
+
const changed = this.#upsertChannelPresenceProfile(
|
|
3410
|
+
channel.channelKey,
|
|
3411
|
+
address,
|
|
3412
|
+
options,
|
|
3413
|
+
now
|
|
3414
|
+
)
|
|
3415
|
+
if (changed && this.#isChannelPresenceAddressOnline(channel.channelKey, address)) {
|
|
3416
|
+
return this.#emitChannelPresence(channel.channelKey, address, 'profile')
|
|
3417
|
+
}
|
|
3418
|
+
return null
|
|
3419
|
+
}
|
|
3420
|
+
|
|
3421
|
+
#removeChannelPresenceSessions(channelKey, options = {}) {
|
|
3422
|
+
const address = normalizeOwnerAddress(options.address)
|
|
3423
|
+
const sourceId = this.#normalizePresenceSourceId(options)
|
|
3424
|
+
const sessionId = options.sessionId
|
|
3425
|
+
? this.#normalizePresenceSessionId(options.sessionId)
|
|
3426
|
+
: ''
|
|
3427
|
+
const sessions = this.#channelPresenceSessions.get(channelKey)
|
|
3428
|
+
if (!sessions || (!address && !sourceId)) return []
|
|
3429
|
+
|
|
3430
|
+
const touchedAddresses = new Set()
|
|
3431
|
+
for (const [key, session] of [...sessions]) {
|
|
3432
|
+
if (address && session.address !== address) continue
|
|
3433
|
+
if (sourceId && session.sourceId !== sourceId) continue
|
|
3434
|
+
if (sessionId && session.sessionId !== sessionId) continue
|
|
3435
|
+
touchedAddresses.add(session.address)
|
|
3436
|
+
sessions.delete(key)
|
|
3437
|
+
}
|
|
3438
|
+
if (sessions.size === 0) {
|
|
3439
|
+
this.#channelPresenceSessions.delete(channelKey)
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
return [...touchedAddresses]
|
|
3443
|
+
.filter(item => !this.#isChannelPresenceAddressOnline(channelKey, item))
|
|
3444
|
+
.map(item => this.#emitChannelPresence(channelKey, item, 'offline'))
|
|
3445
|
+
.filter(Boolean)
|
|
3446
|
+
}
|
|
3447
|
+
|
|
3448
|
+
#removeChannelPresenceSessionsBySource(sourceId) {
|
|
3449
|
+
const normalizedSourceId = String(sourceId || '').trim()
|
|
3450
|
+
if (!normalizedSourceId) return []
|
|
3451
|
+
const events = []
|
|
3452
|
+
for (const [channelKey, sessions] of [...this.#channelPresenceSessions]) {
|
|
3453
|
+
const touchedAddresses = new Set()
|
|
3454
|
+
for (const [key, session] of [...sessions]) {
|
|
3455
|
+
if (session.sourceId !== normalizedSourceId) continue
|
|
3456
|
+
touchedAddresses.add(session.address)
|
|
3457
|
+
sessions.delete(key)
|
|
3458
|
+
}
|
|
3459
|
+
if (sessions.size === 0) {
|
|
3460
|
+
this.#channelPresenceSessions.delete(channelKey)
|
|
3461
|
+
}
|
|
3462
|
+
for (const address of touchedAddresses) {
|
|
3463
|
+
if (!this.#isChannelPresenceAddressOnline(channelKey, address)) {
|
|
3464
|
+
const event = this.#emitChannelPresence(channelKey, address, 'offline')
|
|
3465
|
+
if (event) events.push(event)
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3469
|
+
return events
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
#pruneStaleChannelPresence(now = Date.now()) {
|
|
3473
|
+
const events = []
|
|
3474
|
+
for (const [channelKey, sessions] of [...this.#channelPresenceSessions]) {
|
|
3475
|
+
const touchedAddresses = new Set()
|
|
3476
|
+
for (const [key, session] of [...sessions]) {
|
|
3477
|
+
if (now - session.lastSeen <= CHANNEL_PRESENCE_TIMEOUT_MS) continue
|
|
3478
|
+
touchedAddresses.add(session.address)
|
|
3479
|
+
sessions.delete(key)
|
|
3480
|
+
}
|
|
3481
|
+
if (sessions.size === 0) {
|
|
3482
|
+
this.#channelPresenceSessions.delete(channelKey)
|
|
3483
|
+
}
|
|
3484
|
+
for (const address of touchedAddresses) {
|
|
3485
|
+
if (!this.#isChannelPresenceAddressOnline(channelKey, address)) {
|
|
3486
|
+
const event = this.#emitChannelPresence(channelKey, address, 'offline')
|
|
3487
|
+
if (event) events.push(event)
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
return events
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
#startChannelPresenceSweeper() {
|
|
3495
|
+
if (this.#channelPresenceSweepTimer) return
|
|
3496
|
+
this.#channelPresenceSweepTimer = setInterval(() => {
|
|
3497
|
+
this.#pruneStaleChannelPresence()
|
|
3498
|
+
}, CHANNEL_PRESENCE_HEARTBEAT_MS)
|
|
3499
|
+
this.#channelPresenceSweepTimer.unref?.()
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3502
|
+
#clearChannelPresenceRuntime() {
|
|
3503
|
+
if (this.#channelPresenceSweepTimer) {
|
|
3504
|
+
clearInterval(this.#channelPresenceSweepTimer)
|
|
3505
|
+
this.#channelPresenceSweepTimer = null
|
|
3506
|
+
}
|
|
3507
|
+
this.#channelPresenceSessions.clear()
|
|
3508
|
+
this.#channelPresenceProfiles.clear()
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3104
3511
|
async #getNextChannelMessageTimestamp(channelKey) {
|
|
3105
3512
|
const coresMap = this.#channelCores.get(channelKey)
|
|
3106
3513
|
let maxTimestamp = 0
|
|
@@ -4429,6 +4836,7 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4429
4836
|
type: channel.type,
|
|
4430
4837
|
createdAt: channel.createdAt,
|
|
4431
4838
|
lastMessageAt: channel.lastMessageAt || '',
|
|
4839
|
+
memberAddresses: this.#getChannelMemberAddresses(channel),
|
|
4432
4840
|
writerCoreKeys: uniqueStrings([
|
|
4433
4841
|
...(channel.writerCoreKeys || []),
|
|
4434
4842
|
this.#channelLocalCoreKey.get(channel.channelKey),
|
|
@@ -4462,9 +4870,44 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4462
4870
|
}
|
|
4463
4871
|
}
|
|
4464
4872
|
|
|
4873
|
+
#sendChannelPresence(stream, event) {
|
|
4874
|
+
if (!stream || stream.destroyed || stream.writableEnded || !event) {
|
|
4875
|
+
this.#channelStreams.delete(stream)
|
|
4876
|
+
return false
|
|
4877
|
+
}
|
|
4878
|
+
try {
|
|
4879
|
+
stream.write(
|
|
4880
|
+
`${JSON.stringify({
|
|
4881
|
+
type: 'channel-presence',
|
|
4882
|
+
peerId: this.getNodeId(),
|
|
4883
|
+
channelId: event.channelId,
|
|
4884
|
+
channelKey: event.channelKey,
|
|
4885
|
+
address: event.address,
|
|
4886
|
+
status: event.status,
|
|
4887
|
+
displayName: event.displayName,
|
|
4888
|
+
avatar: event.avatar,
|
|
4889
|
+
profileUpdatedAt: event.profileUpdatedAt,
|
|
4890
|
+
lastSeen: event.lastSeen || Date.now(),
|
|
4891
|
+
sessionId: event.sessionId || 'default',
|
|
4892
|
+
})}\n`
|
|
4893
|
+
)
|
|
4894
|
+
return true
|
|
4895
|
+
} catch {
|
|
4896
|
+
this.#channelStreams.delete(stream)
|
|
4897
|
+
return false
|
|
4898
|
+
}
|
|
4899
|
+
}
|
|
4900
|
+
|
|
4901
|
+
#broadcastChannelPresence(event) {
|
|
4902
|
+
for (const stream of [...this.#channelStreams]) {
|
|
4903
|
+
this.#sendChannelPresence(stream, event)
|
|
4904
|
+
}
|
|
4905
|
+
}
|
|
4906
|
+
|
|
4465
4907
|
async #processChannelHelloMessage(msg) {
|
|
4466
4908
|
if (msg.type !== 'channel-hello') return null
|
|
4467
4909
|
|
|
4910
|
+
const onlineChannels = []
|
|
4468
4911
|
const remoteChannels = Array.isArray(msg.channels)
|
|
4469
4912
|
? msg.channels
|
|
4470
4913
|
.filter(channel => channel && typeof channel === 'object')
|
|
@@ -4480,6 +4923,13 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4480
4923
|
typeof channel.lastMessageAt === 'string'
|
|
4481
4924
|
? channel.lastMessageAt
|
|
4482
4925
|
: '',
|
|
4926
|
+
memberAddresses: uniqueStrings(
|
|
4927
|
+
Array.isArray(channel.memberAddresses)
|
|
4928
|
+
? channel.memberAddresses.map(address =>
|
|
4929
|
+
normalizeOwnerAddress(address)
|
|
4930
|
+
)
|
|
4931
|
+
: []
|
|
4932
|
+
),
|
|
4483
4933
|
writerCoreKeys: uniqueStrings(channel.writerCoreKeys),
|
|
4484
4934
|
}
|
|
4485
4935
|
})
|
|
@@ -4503,9 +4953,15 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4503
4953
|
peers.set(msg.peerId, {
|
|
4504
4954
|
peerId: msg.peerId,
|
|
4505
4955
|
authorName: msg.authorName,
|
|
4956
|
+
memberAddresses: remoteChannel.memberAddresses,
|
|
4506
4957
|
lastSeen: Date.now(),
|
|
4507
4958
|
})
|
|
4508
4959
|
}
|
|
4960
|
+
onlineChannels.push({
|
|
4961
|
+
channelKey: localChannel.channelKey,
|
|
4962
|
+
channelId: localChannel.channelId,
|
|
4963
|
+
memberAddresses: remoteChannel.memberAddresses,
|
|
4964
|
+
})
|
|
4509
4965
|
|
|
4510
4966
|
for (const writerCoreKey of remoteChannel.writerCoreKeys) {
|
|
4511
4967
|
if (
|
|
@@ -4523,11 +4979,51 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4523
4979
|
this.emit('channel:peer:online', {
|
|
4524
4980
|
peerId: msg.peerId,
|
|
4525
4981
|
authorName: msg.authorName,
|
|
4982
|
+
channels: onlineChannels,
|
|
4526
4983
|
})
|
|
4527
4984
|
|
|
4528
4985
|
return msg.peerId
|
|
4529
4986
|
}
|
|
4530
4987
|
|
|
4988
|
+
#processChannelPresenceMessage(msg) {
|
|
4989
|
+
if (msg.type !== 'channel-presence') return null
|
|
4990
|
+
const peerId = String(msg.peerId || '').trim()
|
|
4991
|
+
if (!peerId || peerId === this.getNodeId()) return null
|
|
4992
|
+
const channelId = normalizeChannelId(msg.channelId || msg.channelKey)
|
|
4993
|
+
const channelKey = buildChannelKey(channelId)
|
|
4994
|
+
const localChannel = this.#channels.find(
|
|
4995
|
+
channel => channel.channelKey === channelKey
|
|
4996
|
+
)
|
|
4997
|
+
if (!localChannel) return peerId
|
|
4998
|
+
|
|
4999
|
+
const address = normalizeOwnerAddress(msg.address)
|
|
5000
|
+
if (!address) return peerId
|
|
5001
|
+
|
|
5002
|
+
const status = String(msg.status || '').trim()
|
|
5003
|
+
const options = {
|
|
5004
|
+
address,
|
|
5005
|
+
sessionId: msg.sessionId,
|
|
5006
|
+
sourcePeerId: peerId,
|
|
5007
|
+
local: false,
|
|
5008
|
+
displayName: msg.displayName,
|
|
5009
|
+
avatar: msg.avatar,
|
|
5010
|
+
profileUpdatedAt: msg.profileUpdatedAt,
|
|
5011
|
+
lastSeen: Number(msg.lastSeen) || Date.now(),
|
|
5012
|
+
}
|
|
5013
|
+
|
|
5014
|
+
if (status === 'online') {
|
|
5015
|
+
this.#upsertChannelPresenceSession(localChannel, options)
|
|
5016
|
+
} else if (status === 'heartbeat') {
|
|
5017
|
+
this.#touchChannelPresenceSession(localChannel, options)
|
|
5018
|
+
} else if (status === 'profile') {
|
|
5019
|
+
this.#updateChannelPresenceProfile(localChannel, options)
|
|
5020
|
+
} else if (status === 'offline') {
|
|
5021
|
+
this.#removeChannelPresenceSessions(localChannel.channelKey, options)
|
|
5022
|
+
}
|
|
5023
|
+
|
|
5024
|
+
return peerId
|
|
5025
|
+
}
|
|
5026
|
+
|
|
4531
5027
|
async #handleChannelConnection(conn) {
|
|
4532
5028
|
const stream = conn
|
|
4533
5029
|
let connectedPeerId = null
|
|
@@ -4546,9 +5042,11 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4546
5042
|
newlineIndex = readBuffer.indexOf('\n')
|
|
4547
5043
|
if (!line) continue
|
|
4548
5044
|
try {
|
|
4549
|
-
const
|
|
4550
|
-
|
|
4551
|
-
|
|
5045
|
+
const message = JSON.parse(line)
|
|
5046
|
+
const peerId =
|
|
5047
|
+
message.type === 'channel-presence'
|
|
5048
|
+
? this.#processChannelPresenceMessage(message)
|
|
5049
|
+
: await this.#processChannelHelloMessage(message)
|
|
4552
5050
|
if (peerId) connectedPeerId = peerId
|
|
4553
5051
|
} catch (err) {
|
|
4554
5052
|
console.warn(`[MostBox] Failed to process channel data:`, err.message)
|
|
@@ -4561,16 +5059,23 @@ export class MostBoxEngine extends EventEmitter {
|
|
|
4561
5059
|
closed = true
|
|
4562
5060
|
this.#channelStreams.delete(stream)
|
|
4563
5061
|
if (connectedPeerId) {
|
|
4564
|
-
for (const [, peers] of this.#channelPeers) {
|
|
5062
|
+
for (const [channelKey, peers] of this.#channelPeers) {
|
|
4565
5063
|
if (peers.has(connectedPeerId)) {
|
|
4566
5064
|
const peer = peers.get(connectedPeerId)
|
|
4567
5065
|
peers.delete(connectedPeerId)
|
|
5066
|
+
const channel = this.#channels.find(
|
|
5067
|
+
item => item.channelKey === channelKey
|
|
5068
|
+
)
|
|
4568
5069
|
this.emit('channel:peer:offline', {
|
|
4569
5070
|
peerId: connectedPeerId,
|
|
4570
5071
|
authorName: peer?.authorName,
|
|
5072
|
+
channelKey,
|
|
5073
|
+
channelId: channel?.channelId || '',
|
|
5074
|
+
memberAddresses: peer?.memberAddresses || [],
|
|
4571
5075
|
})
|
|
4572
5076
|
}
|
|
4573
5077
|
}
|
|
5078
|
+
this.#removeChannelPresenceSessionsBySource(`peer:${connectedPeerId}`)
|
|
4574
5079
|
}
|
|
4575
5080
|
}
|
|
4576
5081
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
._page_1yyew_1{background:radial-gradient(circle at 35% 25%, #34c75924, transparent 32%), radial-gradient(circle at 78% 18%, #ff95001f, transparent 28%), var(--bg-primary);flex:1;min-height:0;padding:20px;overflow:auto}._headerActions_1yyew_12{align-items:center;gap:8px;display:flex}._entry_1yyew_18{grid-template-columns:minmax(0,1fr) 360px;align-items:center;gap:48px;max-width:1040px;min-height:calc(100vh - 116px);margin:0 auto;display:grid}._entryBrand_1yyew_28{align-items:center;gap:22px;display:flex}._cardMark_1yyew_34{width:112px;height:152px;box-shadow:var(--shadow-lg);color:#fff;background:linear-gradient(135deg,#1f8f5f,#284f7a);border:8px solid #fff;border-radius:8px;flex:0 0 112px;place-items:center;display:grid}._entryBrand_1yyew_28 h1{color:var(--text-primary);margin:0;font-size:64px;line-height:.95}._entryBrand_1yyew_28 p{max-width:520px;color:var(--text-secondary);margin:18px 0 0;font-size:18px;line-height:1.7}._entryPanel_1yyew_62{border:1px solid var(--glass-border);background:var(--glass-bg);box-shadow:var(--glass-shadow);border-radius:8px;gap:16px;padding:22px;display:grid}._accountLine_1yyew_72{color:var(--text-secondary);justify-content:space-between;align-items:center;gap:10px;font-size:14px;display:flex}._accountLine_1yyew_72 strong{max-width:190px;color:var(--text-primary);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}._joinForm_1yyew_89,._joinForm_1yyew_89 label{gap:8px;display:grid}._joinForm_1yyew_89 label{color:var(--text-secondary);font-size:14px}._joinForm_1yyew_89 input{border:1px solid var(--glass-border);background:var(--bg-primary);width:100%;min-height:40px;color:var(--text-primary);border-radius:8px;padding:0 12px}._status_1yyew_110{color:var(--text-secondary);margin:0;font-size:14px}._board_1yyew_116{grid-template-columns:minmax(0,1fr) minmax(320px,380px);gap:18px;min-height:100%;display:grid}._table_1yyew_123{background:linear-gradient(145deg, #0a533ee6, #163550eb), var(--glass-bg);min-height:560px;box-shadow:var(--shadow-lg);border:1px solid #ffffff2e;border-radius:8px;position:relative;overflow:hidden}._table_1yyew_123:before{content:"";pointer-events:none;border:2px solid #ffffff2e;border-radius:8px;position:absolute;inset:24px}._tableCenter_1yyew_144{color:#fff;background:#00000038;border:1px solid #ffffff38;border-radius:50%;flex-direction:column;justify-content:center;align-items:center;gap:5px;width:150px;height:150px;display:flex;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);box-shadow:inset 0 0 24px #00000029}._tableCenter_1yyew_144 span,._tableCenter_1yyew_144 small{opacity:.78;font-size:12px}._tableCenter_1yyew_144 strong{font-size:32px}._finishBanner_1yyew_173{z-index:2;color:#fff;white-space:nowrap;background:linear-gradient(135deg,#30d158e6,#34c759e6);border-radius:20px;align-items:center;gap:8px;padding:10px 20px;font-size:15px;font-weight:700;display:flex;position:absolute;top:16px;left:50%;transform:translate(-50%);box-shadow:0 4px 16px #30d15866}._finishBannerIcon_1yyew_192{font-size:20px}._seats_1yyew_196{z-index:1;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;padding:34px;display:grid;position:relative}._seat_1yyew_196{color:#fff;background:#ffffff1a;border:1px solid #ffffff29;border-radius:8px;grid-template-columns:44px minmax(0,1fr);align-items:center;gap:10px;min-height:88px;padding:12px;display:grid}._seatTurn_1yyew_218{border-color:#ffd666eb;box-shadow:0 0 0 2px #ffd6662e}._seatWinner_1yyew_223{border-color:#30d158eb;box-shadow:0 0 0 2px #30d15833}._seatFolded_1yyew_228{opacity:.5}._seatAvatar_1yyew_232{color:#fff;background:#17211b;border-radius:8px;place-items:center;width:44px;height:44px;font-size:15px;font-weight:900;display:grid;overflow:hidden}._seatAvatar_1yyew_232 img{object-fit:cover;width:100%;height:100%}._seatMain_1yyew_251,._seatMeta_1yyew_252{flex-direction:column;gap:4px;min-width:0;display:flex}._seatMain_1yyew_251 strong,._seatMain_1yyew_251 span,._seatMeta_1yyew_252 span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}._seatMain_1yyew_251 strong{font-size:14px}._seatMain_1yyew_251 span,._seatMeta_1yyew_252{opacity:.76;font-size:12px}._seatMeta_1yyew_252{flex-direction:row;grid-column:1/-1;justify-content:space-between}._panel_1yyew_283{flex-direction:column;gap:14px;min-width:0;display:flex}._panelSection_1yyew_290{border:1px solid var(--glass-border);background:var(--glass-bg);-webkit-backdrop-filter:var(--blur-md);box-shadow:var(--glass-shadow);border-radius:8px;padding:16px}._panelTitle_1yyew_300{color:var(--text-primary);align-items:center;gap:8px;margin-bottom:12px;font-size:14px;font-weight:600;display:flex}._hand_1yyew_310{grid-template-columns:repeat(3,62px);justify-content:center;gap:10px;display:grid}._card_1yyew_34{color:#17211b;text-align:left;background:#fff;border:1px solid #dbe1dc;border-radius:8px;grid-template-rows:auto 1fr;width:62px;height:92px;padding:7px;display:grid;box-shadow:0 8px 18px #0e16111f}._card_1yyew_34 span{font-size:15px;font-weight:900}._card_1yyew_34 i{place-self:end;font-size:22px;font-style:normal;font-weight:900}._red_1yyew_344{color:#c93d32}._black_1yyew_348{color:#17211b}._cardBack_1yyew_352{color:#fff;background:repeating-linear-gradient(45deg,#ffffff1f 0 6px,#0000 6px 12px),linear-gradient(135deg,#243f7a,#752f4d);border:1px solid #dbe1dc;border-radius:8px;justify-content:center;align-items:center;width:62px;height:92px;display:flex;box-shadow:0 8px 18px #0e16111f}._handLabel_1yyew_373{text-align:center;color:var(--text-secondary);margin-top:10px;font-size:13px}._actionGrid_1yyew_380,._raiseRow_1yyew_381{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;margin-top:10px;display:grid}._raiseRow_1yyew_381{grid-template-columns:minmax(0,1fr) 96px}._statusGrid_1yyew_392{grid-template-columns:76px minmax(0,1fr);align-items:start;gap:8px 12px;font-size:13px;display:grid}._statusGrid_1yyew_392 span{color:var(--text-secondary)}._statusGrid_1yyew_392 strong{color:var(--text-primary);overflow-wrap:anywhere;font-weight:600}._muted_1yyew_410{color:var(--text-secondary);margin:0;font-size:13px;line-height:1.6}._compareOverlay_1yyew_417{z-index:100;-webkit-backdrop-filter:blur(4px);background:#0009;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}._compareBox_1yyew_429{background:var(--glass-bg);border:1px solid var(--glass-border);box-shadow:var(--shadow-lg);border-radius:16px;flex-direction:column;align-items:center;gap:20px;width:90%;max-width:560px;padding:28px 36px;display:flex}._compareTitle_1yyew_443{color:var(--text-primary);font-size:18px;font-weight:700}._comparePlayers_1yyew_449{align-items:center;gap:20px;width:100%;display:flex}._compareSide_1yyew_456{background:#ffffff0d;border:2px solid #0000;border-radius:12px;flex-direction:column;flex:1;align-items:center;gap:12px;padding:16px;display:flex}._compareWinner_1yyew_468{background:#30d1581a;border-color:#30d158cc}._compareName_1yyew_473{color:var(--text-primary);text-align:center;text-overflow:ellipsis;white-space:nowrap;max-width:100%;font-size:14px;font-weight:600;overflow:hidden}._compareCards_1yyew_484{justify-content:center;gap:8px;display:flex}._compareCards_1yyew_484 ._card_1yyew_34{width:52px;height:76px;padding:5px}._compareCards_1yyew_484 ._card_1yyew_34 span{font-size:13px}._compareCards_1yyew_484 ._card_1yyew_34 i{font-size:18px}._compareCards_1yyew_484 ._cardBack_1yyew_352{width:52px;height:76px}._compareLabel_1yyew_509{color:var(--text-secondary);font-size:12px;font-weight:500}._compareVs_1yyew_515{color:var(--text-secondary);opacity:.6;font-size:20px;font-weight:900}._compareResult_1yyew_522{color:#30d158;font-size:16px;font-weight:700}@media (width<=980px){._entry_1yyew_18{grid-template-columns:1fr;align-content:center}._board_1yyew_116{grid-template-columns:1fr}._table_1yyew_123{min-height:420px}}@media (width<=640px){._page_1yyew_1{padding:12px}._entryBrand_1yyew_28{flex-direction:column;align-items:flex-start}._entryBrand_1yyew_28 h1{font-size:42px}._cardMark_1yyew_34{border-width:6px;flex-basis:100px;width:74px;height:100px}._seats_1yyew_196{grid-template-columns:1fr;padding:22px}._tableCenter_1yyew_144{margin:24px auto 0;position:static;transform:none}._table_1yyew_123:before{inset:14px}._hand_1yyew_310{grid-template-columns:repeat(3,minmax(58px,72px))}._actionGrid_1yyew_380,._raiseRow_1yyew_381{grid-template-columns:1fr}}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
._page_p7qf6_1{background:var(--bg-primary);min-height:calc(100vh - 64px);padding:18px}._headerActions_p7qf6_7{align-items:center;gap:8px;display:flex}._entry_p7qf6_13{grid-template-columns:minmax(0,1fr) 360px;align-items:center;gap:48px;max-width:1040px;min-height:calc(100vh - 116px);margin:0 auto;display:grid}._entryBrand_p7qf6_23{align-items:center;gap:22px;display:flex}._cardMark_p7qf6_29{width:112px;height:152px;box-shadow:var(--shadow-lg);color:#fff;background:#c93d32;border:8px solid #fff;border-radius:8px;flex:0 0 112px;place-items:center;font-size:70px;font-weight:900;display:grid}._entryBrand_p7qf6_23 h1{color:var(--text-primary);margin:0;font-size:64px;line-height:.95}._entryBrand_p7qf6_23 p{max-width:520px;color:var(--text-secondary);margin:18px 0 0;font-size:18px;line-height:1.7}._entryPanel_p7qf6_59,._panel_p7qf6_60,._handPanel_p7qf6_61{border:1px solid var(--glass-border);background:var(--glass-bg);box-shadow:var(--glass-shadow);border-radius:8px}._entryPanel_p7qf6_59{gap:16px;padding:22px;display:grid}._accountLine_p7qf6_74,._scoreMeta_p7qf6_75,._roomBar_p7qf6_76,._badges_p7qf6_77,._actions_p7qf6_78{align-items:center;gap:10px;display:flex}._accountLine_p7qf6_74{color:var(--text-secondary);justify-content:space-between;font-size:14px}._accountLine_p7qf6_74 strong{max-width:190px;color:var(--text-primary);text-overflow:ellipsis;white-space:nowrap;overflow:hidden}._joinForm_p7qf6_98,._joinForm_p7qf6_98 label{gap:8px;display:grid}._joinForm_p7qf6_98 label{color:var(--text-secondary);font-size:14px}._joinForm_p7qf6_98 input{border:1px solid var(--glass-border);background:var(--bg-primary);width:100%;min-height:40px;color:var(--text-primary);border-radius:8px;padding:0 12px}._status_p7qf6_119{color:var(--text-secondary);margin:0;font-size:14px}._gameGrid_p7qf6_125{grid-template:"table side"minmax(360px,1fr)"hand side"/minmax(0,1fr) 320px;gap:14px;min-height:calc(100vh - 104px);display:grid}._tablePanel_p7qf6_136{color:#fff;background:radial-gradient(circle,#ffffff2e,#0000 46%),#247658;border-radius:8px;grid-area:table;grid-template-rows:auto auto 1fr auto;gap:14px;min-height:360px;padding:16px;display:grid}._roomBar_p7qf6_76{justify-content:space-between}._roomBar_p7qf6_76 div:first-child{gap:2px;display:grid}._roomBar_p7qf6_76 span,._badges_p7qf6_77 span{color:#ffffffc7;font-size:13px}._badges_p7qf6_77{flex-wrap:wrap;justify-content:flex-end}._seats_p7qf6_170{grid-template-columns:repeat(auto-fit,minmax(168px,1fr));gap:10px;display:grid}._player_p7qf6_176{color:#17211b;background:#ffffffeb;border:1px solid #17211b1f;border-radius:8px;align-items:center;gap:10px;min-width:0;padding:8px;display:flex}._tablePanel_p7qf6_136 ._player_p7qf6_176{color:#fff;background:#ffffff2e}._active_p7qf6_193{outline:3px solid #f3cf5a}._winner_p7qf6_197{outline:3px solid #7be39e}._avatar_p7qf6_201{color:#fff;background:#17211b;border-radius:50%;flex:0 0 40px;place-items:center;width:40px;height:40px;font-size:13px;font-weight:900;display:grid;overflow:hidden}._avatar_p7qf6_201 img{object-fit:cover;width:100%;height:100%}._player_p7qf6_176 strong,._player_p7qf6_176 span{text-overflow:ellipsis;white-space:nowrap;display:block;overflow:hidden}._player_p7qf6_176 span{opacity:.76;max-width:190px;font-size:12px}._centerTable_p7qf6_235{grid-template-columns:120px minmax(0,1fr) 120px;align-items:center;gap:14px;display:grid}._deckBox_p7qf6_242,._playedBox_p7qf6_243{text-align:center;background:#0b221942;border-radius:8px}._deckBox_p7qf6_242{gap:6px;padding:18px 8px;display:grid}._deckBox_p7qf6_242 span{opacity:.75;font-size:13px}._deckBox_p7qf6_242 strong{font-size:30px}._playedBox_p7qf6_243{place-items:center;min-height:150px;padding:16px;display:grid}._playedCards_p7qf6_271{flex-wrap:wrap;justify-content:center;gap:8px;margin-top:10px;display:flex}._notice_p7qf6_279{text-align:center;background:#0000002e;border-radius:8px;padding:10px 12px;font-weight:800}._resultDetail_p7qf6_287{gap:4px;font-weight:600;display:grid}._resultDetail_p7qf6_287 p{margin:0;font-size:14px}._sidePanel_p7qf6_298{grid-area:side;align-content:start;gap:12px;display:grid}._panel_p7qf6_60{gap:12px;padding:14px;display:grid}._panel_p7qf6_60 h3{color:var(--text-primary);margin:0;font-size:15px}._scoreMeta_p7qf6_75{color:var(--text-secondary);justify-content:space-between;font-size:13px}._scoreRow_p7qf6_323{border-top:1px solid var(--glass-border);color:var(--text-primary);justify-content:space-between;padding-top:8px;display:flex}._logPanel_p7qf6_331{max-height:280px;overflow:auto}._logPanel_p7qf6_331 p{color:var(--text-secondary);margin:0;font-size:13px;line-height:1.5}._handPanel_p7qf6_61{grid-area:hand;gap:12px;padding:12px;display:grid}._hand_p7qf6_61{gap:8px;min-height:118px;padding:4px 2px 10px;display:flex;overflow-x:auto}._cardButton_p7qf6_358{background:0 0;border:0;border-radius:8px;flex:none;min-height:auto;padding:0;transition:transform .16s}._picked_p7qf6_368{transform:translateY(-14px)}._card_p7qf6_29{color:#17211b;text-align:left;background:#fff;border:1px solid #dbe1dc;border-radius:8px;grid-template-rows:auto 1fr;width:62px;height:92px;padding:7px;display:grid;box-shadow:0 8px 18px #0e16111f}._card_p7qf6_29 span{font-size:15px;font-weight:900}._card_p7qf6_29 i{place-self:end;font-size:22px;font-style:normal;font-weight:900}._red_p7qf6_399{color:#c93d32}._black_p7qf6_403{color:#17211b}._small_p7qf6_407{width:46px;height:66px;padding:5px}._small_p7qf6_407 span{font-size:12px}._small_p7qf6_407 i{font-size:15px}._actions_p7qf6_78{flex-wrap:wrap}._preview_p7qf6_425{border:1px solid var(--glass-border);background:var(--bg-primary);min-height:46px;color:var(--text-secondary);border-radius:8px;flex:260px;padding:7px 10px;display:grid}._preview_p7qf6_425 strong{color:var(--text-primary);font-size:14px}._preview_p7qf6_425 span{text-overflow:ellipsis;white-space:nowrap;font-size:12px;overflow:hidden}._valid_p7qf6_448{border-color:#176b4d70}@media (width<=980px){._entry_p7qf6_13{grid-template-columns:1fr;align-content:center}._gameGrid_p7qf6_125{grid-template-columns:1fr;grid-template-areas:"table""hand""side"}}@media (width<=640px){._page_p7qf6_1{padding:10px}._entryBrand_p7qf6_23{flex-direction:column;align-items:flex-start}._entryBrand_p7qf6_23 h1{font-size:42px}._cardMark_p7qf6_29{border-width:6px;flex-basis:100px;width:74px;height:100px;font-size:46px}._centerTable_p7qf6_235{grid-template-columns:1fr}._actions_p7qf6_78>button{flex:110px}}
|