|
|
|
@ -35,8 +35,8 @@ |
|
|
|
<view class="timer-label">请勿发送涉黄涉暴反动侮辱等内容</view> |
|
|
|
<view class="timer-title">发送图片、音视频消息涉及内容审核,可能略有延迟</view> |
|
|
|
</view> |
|
|
|
<view class="timer-pill" :class="{ streak: streak.streakDays > 0, expiring: streak.expiring }" @tap="streak.streakDays > 0 && showStreakTip()"> |
|
|
|
{{ streak.streakDays > 0 ? '🔥x' + streak.streakDays : '已连接' }} |
|
|
|
<view class="timer-pill" :class="{ streak: streak.streakDays > 0, zero: !streak.streakDays, expiring: streak.expiring }" @tap="streak.streakDays > 0 && showStreakTip()"> |
|
|
|
🔥x{{ streak.streakDays || 0 }} |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
|
|
|
|
@ -71,13 +71,15 @@ |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
<view class="bubble emoji-bubble" v-else-if="item.messageType === 3" :class="{ muted: item.pending, blocked: item.blocked }">{{ item.content }}</view> |
|
|
|
<view class="bubble voice-bubble" v-else-if="item.messageType === 4" :class="{ muted: item.pending, blocked: item.blocked, playing: playingVoiceUrl === item.content }" |
|
|
|
<view class="bubble voice-bubble" v-else-if="item.messageType === 4" :style="{ width: voiceBubbleWidth(item) }" :class="{ muted: item.pending, blocked: item.blocked, playing: playingVoiceUrl === item.content }" |
|
|
|
@tap="playVoice(item)"> |
|
|
|
<text class="voice-wave">{{ playingVoiceUrl === item.content ? '▮▮▮' : '▮▯▮' }}</text> |
|
|
|
<text>{{ item.mediaDuration || 1 }}''</text> |
|
|
|
</view> |
|
|
|
<view class="bubble" v-else :class="{ muted: item.pending, blocked: item.blocked }">{{ item.content }}</view> |
|
|
|
<view class="blocked-tip" v-if="item.mine && item.blocked">{{ item.blockTip || '内容未通过审核,请换一个更合适的内容' }}</view> |
|
|
|
<view class="message-status" v-if="item.mine && messageStatusText(item)" :class="{ blocked: item.blocked }"> |
|
|
|
{{ messageStatusText(item) }} |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
</view> |
|
|
|
<view class="hesitate-line right" v-if="typingHint">{{ typingHint }}</view> |
|
|
|
@ -92,20 +94,25 @@ |
|
|
|
</view> |
|
|
|
|
|
|
|
<view class="emoji-panel" v-if="showEmoji" :style="{ bottom: emojiPanelBottom }"> |
|
|
|
<view class="emoji-item" v-for="item in emojis" :key="item" @tap="sendEmoji(item)">{{ item }}</view> |
|
|
|
<scroll-view scroll-y class="emoji-scroll"> |
|
|
|
<view class="emoji-grid"> |
|
|
|
<view class="emoji-item" v-for="item in emojis" :key="item" @tap="sendEmoji(item)">{{ item }}</view> |
|
|
|
</view> |
|
|
|
</scroll-view> |
|
|
|
</view> |
|
|
|
<view class="emoji-close-mask" v-if="showEmoji" @tap="closeEmoji"></view> |
|
|
|
|
|
|
|
<view class="input-bar" :style="{ bottom: inputBarBottom }"> |
|
|
|
<view class="tool-btn" @tap="toggleEmoji">☺</view> |
|
|
|
<view class="tool-btn" @tap="toggleVoiceMode">{{ voiceMode ? '键' : '语' }}</view> |
|
|
|
<input class="input" v-if="!voiceMode" v-model="draft" :disabled="isBlocked" :placeholder="isBlocked ? '已加入黑名单,无法继续发送' : '轻轻说一句,或保持安静'" |
|
|
|
:adjust-position="false" :cursor-spacing="20" @focus="handleInputFocus" @blur="handleInputBlur" /> |
|
|
|
<view class="tool-btn voice-toggle" @tap="toggleVoiceMode">{{ voiceMode ? '⌨' : '🎙' }}</view> |
|
|
|
<input class="input" v-if="!voiceMode" v-model="draft" :focus="inputFocus" hold-keyboard="true" :confirm-hold="true" confirm-type="send" :disabled="isBlocked" :placeholder="isBlocked ? '已加入黑名单,无法继续发送' : '轻轻说一句,或保持安静'" |
|
|
|
:adjust-position="false" :cursor-spacing="20" @focus="handleInputFocus" @blur="handleInputBlur" @confirm="sendMessage" /> |
|
|
|
<view class="voice-hold" v-else :class="{ recording: recording, cancelling: voiceCancelling }" |
|
|
|
@touchstart.prevent="startRecord" @touchmove.prevent="moveRecord" @touchend="stopRecord" @touchcancel="cancelRecord"> |
|
|
|
{{ voiceHoldText }} |
|
|
|
</view> |
|
|
|
<view class="tool-btn" @tap="chooseMedia">+</view> |
|
|
|
<view class="send" v-if="!voiceMode" :class="{ disabled: isBlocked }" @tap="sendMessage">发送</view> |
|
|
|
<view class="send" v-if="!voiceMode && !keyboardVisible" :class="{ disabled: isBlocked }" @tap="sendMessage">发送</view> |
|
|
|
</view> |
|
|
|
|
|
|
|
<view class="safety-mask" v-if="showSafety" @tap="showSafety = false"> |
|
|
|
@ -235,6 +242,7 @@ |
|
|
|
voiceMode: false, |
|
|
|
typingHint: '', |
|
|
|
finishing: false, |
|
|
|
reporting: false, |
|
|
|
uploadingImage: false, |
|
|
|
uploadingVoice: false, |
|
|
|
recording: false, |
|
|
|
@ -250,6 +258,7 @@ |
|
|
|
voiceSwitching: false, |
|
|
|
keyboardHeight: 0, |
|
|
|
keyboardListener: null, |
|
|
|
inputFocus: false, |
|
|
|
streak: { streakDays: 0, bothToday: false, expiring: false }, |
|
|
|
lastStreakLoad: 0, |
|
|
|
showQuiz: false, |
|
|
|
@ -265,7 +274,7 @@ |
|
|
|
scrollIntoView: '', |
|
|
|
scrollAnimation: true, |
|
|
|
sendingMessage: false, |
|
|
|
emojis: ['🙂', '😄', '🥲', '😭', '😴', '🙌', '🌙', '☁️', '🍃', '✨', '💛', '🫶'], |
|
|
|
emojis: ['🙂', '😄', '😁', '😂', '🤣', '😊', '🥰', '😍', '😘', '😎', '🤔', '😳', '🥺', '😭', '😤', '😴', '🤯', '😇', '🙌', '👏', '👍', '👌', '🙏', '💪', '👀', '🤝', '🌙', '☁️', '🍃', '✨', '💛', '🫶', '🌸', '🌈', '🔥', '🎉', '🎧', '📚', '🍚', '🧋', '🚶', '🐱'], |
|
|
|
presenceActions: ['我在', '听着呢', '慢慢说', '抱一下空气'], |
|
|
|
reportReasons: [ |
|
|
|
{ key: 'harassment', label: '骚扰不适' }, |
|
|
|
@ -317,7 +326,7 @@ |
|
|
|
return this.keyboardHeight > 0 ? (this.keyboardHeight + 8) + 'px' : '30rpx' |
|
|
|
}, |
|
|
|
bottomSafePadding() { |
|
|
|
return this.keyboardHeight > 0 ? (this.keyboardHeight + 188) + 'px' : '260rpx' |
|
|
|
return this.keyboardHeight > 0 ? (this.keyboardHeight + 138) + 'px' : '230rpx' |
|
|
|
}, |
|
|
|
inputBarBottom() { |
|
|
|
return this.keyboardBottomPx |
|
|
|
@ -326,7 +335,10 @@ |
|
|
|
return this.keyboardHeight > 0 ? (this.keyboardHeight + 76) + 'px' : '132rpx' |
|
|
|
}, |
|
|
|
emojiPanelBottom() { |
|
|
|
return this.keyboardHeight > 0 ? (this.keyboardHeight + 82) + 'px' : '128rpx' |
|
|
|
return this.keyboardHeight > 0 ? (this.keyboardHeight + 108) + 'px' : '138rpx' |
|
|
|
}, |
|
|
|
keyboardVisible() { |
|
|
|
return this.keyboardHeight > 0 |
|
|
|
}, |
|
|
|
messagesStyle() { |
|
|
|
return {} |
|
|
|
@ -384,6 +396,10 @@ |
|
|
|
}, |
|
|
|
onShow() { |
|
|
|
if (this.roomId) { |
|
|
|
if (!this.messages.length) { |
|
|
|
const localFallback = this.mergeLocalMessages([]) |
|
|
|
if (localFallback.length) this.messages = localFallback |
|
|
|
} |
|
|
|
this.startMessagePolling() |
|
|
|
this.startQuizPolling() |
|
|
|
this.pullNewMessages() |
|
|
|
@ -410,6 +426,10 @@ |
|
|
|
} |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
voiceBubbleWidth(item) { |
|
|
|
const duration = Math.max(1, Math.min(60, Number(item && item.mediaDuration) || 1)) |
|
|
|
return (170 + Math.min(230, duration * 5)) + 'rpx' |
|
|
|
}, |
|
|
|
bindKeyboardListener() { |
|
|
|
if (!uni.onKeyboardHeightChange) return |
|
|
|
this.keyboardListener = (res = {}) => { |
|
|
|
@ -417,6 +437,9 @@ |
|
|
|
this.$nextTick(() => { |
|
|
|
this.scrollToBottom(false) |
|
|
|
}) |
|
|
|
setTimeout(() => { |
|
|
|
this.scrollToBottom(false) |
|
|
|
}, 180) |
|
|
|
} |
|
|
|
uni.onKeyboardHeightChange(this.keyboardListener) |
|
|
|
}, |
|
|
|
@ -428,6 +451,8 @@ |
|
|
|
this.keyboardListener = null |
|
|
|
}, |
|
|
|
handleInputFocus(e) { |
|
|
|
this.closeEmoji() |
|
|
|
this.inputFocus = true |
|
|
|
const height = e && e.detail ? e.detail.height : 0 |
|
|
|
if (height) { |
|
|
|
this.keyboardHeight = height |
|
|
|
@ -437,10 +462,63 @@ |
|
|
|
}) |
|
|
|
}, |
|
|
|
handleInputBlur() { |
|
|
|
this.inputFocus = false |
|
|
|
setTimeout(() => { |
|
|
|
this.keyboardHeight = 0 |
|
|
|
this.scrollToBottom(false) |
|
|
|
}, 120) |
|
|
|
}, |
|
|
|
localMessageKey() { |
|
|
|
return this.roomId ? 'ie_chat_local_messages_' + this.roomId : '' |
|
|
|
}, |
|
|
|
saveLocalMessages() { |
|
|
|
const key = this.localMessageKey() |
|
|
|
if (!key) return |
|
|
|
const locals = this.messages.filter(item => item && item.localOnly && item.createAt && Date.now() - item.createAt < 30 * 60 * 1000) |
|
|
|
if (locals.length) { |
|
|
|
// 只保留最近少量临时态,服务端 ACK 后会清理;不会长期堆小程序缓存 |
|
|
|
uni.setStorageSync(key, locals.slice(-20)) |
|
|
|
} else { |
|
|
|
uni.removeStorageSync(key) |
|
|
|
} |
|
|
|
}, |
|
|
|
loadLocalMessages() { |
|
|
|
const key = this.localMessageKey() |
|
|
|
if (!key) return [] |
|
|
|
const list = uni.getStorageSync(key) |
|
|
|
return Array.isArray(list) ? list : [] |
|
|
|
}, |
|
|
|
removeLocalMessage(clientMsgId) { |
|
|
|
if (!clientMsgId) return |
|
|
|
this.messages = this.messages.filter(item => item.clientMsgId !== clientMsgId) |
|
|
|
this.saveLocalMessages() |
|
|
|
}, |
|
|
|
mergeLocalMessages(remoteMessages) { |
|
|
|
const remote = remoteMessages || [] |
|
|
|
const exists = new Set() |
|
|
|
remote.forEach(item => { |
|
|
|
if (item.messageId) exists.add('m:' + String(item.messageId)) |
|
|
|
if (item.clientMsgId) exists.add('c:' + item.clientMsgId) |
|
|
|
}) |
|
|
|
const locals = this.loadLocalMessages().filter(item => { |
|
|
|
if (item.messageId && exists.has('m:' + String(item.messageId))) return false |
|
|
|
if (item.clientMsgId && exists.has('c:' + item.clientMsgId)) return false |
|
|
|
if (item.localState === 'uploading' && item.createAt && Date.now() - item.createAt > 8 * 60 * 1000) { |
|
|
|
item.localState = 'failed' |
|
|
|
item.blocked = true |
|
|
|
item.pending = false |
|
|
|
item.blockTip = '上传中断,请重新发送' |
|
|
|
} |
|
|
|
return item.localOnly |
|
|
|
}) |
|
|
|
return remote.concat(locals) |
|
|
|
}, |
|
|
|
messageStatusText(item) { |
|
|
|
if (!item || !item.mine) return '' |
|
|
|
if (item.blocked) return item.blockTip || '内容未通过审核,请换一个更合适的内容' |
|
|
|
if (item.localState === 'failed') return item.blockTip || '发送失败,请重新发送' |
|
|
|
return '' |
|
|
|
}, |
|
|
|
async loadBlockStatus() { |
|
|
|
if (!this.roomId) return |
|
|
|
const status = await getIeRoomBlockStatus(this.roomId) |
|
|
|
@ -474,8 +552,11 @@ |
|
|
|
mediaFormat: item.mediaFormat, |
|
|
|
poster: item.poster || item.thumbTempFilePath || '', |
|
|
|
mine, |
|
|
|
pending: false, |
|
|
|
pending: item.mediaAuditStatus === 1, |
|
|
|
blocked: item.isBlocked === 1, |
|
|
|
blockTip: item.auditTip || (item.isBlocked === 1 ? '内容未通过审核,请换一个更合适的内容' : ''), |
|
|
|
localState: '', |
|
|
|
localOnly: false, |
|
|
|
mediaAuditStatus: item.mediaAuditStatus || 0 |
|
|
|
} |
|
|
|
}, |
|
|
|
@ -585,8 +666,13 @@ |
|
|
|
}, |
|
|
|
async pullNewMessages() { |
|
|
|
if (!this.roomId || this.loadingMessages) return |
|
|
|
const page = await pageIeMessages(this.roomId, 1, this.messagePageSize) |
|
|
|
const latest = this.normalizePage(page) |
|
|
|
let latest = [] |
|
|
|
try { |
|
|
|
const page = await pageIeMessages(this.roomId, 1, this.messagePageSize) |
|
|
|
latest = this.normalizePage(page) |
|
|
|
} catch (e) { |
|
|
|
return |
|
|
|
} |
|
|
|
if (!latest.length) return |
|
|
|
// 本地刚发送的消息在 ack 前没有 messageId,去重必须同时核对两种 id,避免心跳把自己的消息拉成重复 |
|
|
|
const exists = new Set() |
|
|
|
@ -607,10 +693,16 @@ |
|
|
|
(remote.clientMsgId && item.clientMsgId === remote.clientMsgId)) |
|
|
|
if (!local) return |
|
|
|
const wasBlocked = !!local.blocked |
|
|
|
local.pending = false |
|
|
|
local.pending = remote.mediaAuditStatus === 1 |
|
|
|
local.blocked = !!remote.blocked |
|
|
|
local.blockTip = remote.blockTip || (local.blocked ? '内容未通过审核,请换一个更合适的内容' : '') |
|
|
|
local.mediaAuditStatus = remote.mediaAuditStatus || local.mediaAuditStatus || 0 |
|
|
|
local.messageId = remote.messageId || local.messageId |
|
|
|
local.poster = remote.poster || local.poster || '' |
|
|
|
local.localOnly = false |
|
|
|
local.localState = '' |
|
|
|
if (local.messageType === 1 || local.messageType === 3) local.content = remote.content || local.content |
|
|
|
this.saveLocalMessages() |
|
|
|
if (!wasBlocked && local.blocked && local.mine) { |
|
|
|
uni.showToast({ title: '内容未通过审核,请换一个更合适的内容', icon: 'none' }) |
|
|
|
} |
|
|
|
@ -626,9 +718,18 @@ |
|
|
|
this.messagePage = 1 |
|
|
|
this.loadingMessages = true |
|
|
|
try { |
|
|
|
const localFallback = this.mergeLocalMessages([]) |
|
|
|
if (!this.messages.length && localFallback.length) { |
|
|
|
this.messages = localFallback |
|
|
|
this.scrollToBottom(false) |
|
|
|
} |
|
|
|
const page = await pageIeMessages(this.roomId, this.messagePage, this.messagePageSize) |
|
|
|
const latest = this.normalizePage(page) |
|
|
|
this.messages = latest |
|
|
|
if (!latest.length && this.messages.length) { |
|
|
|
this.hasMoreMessages = false |
|
|
|
return |
|
|
|
} |
|
|
|
this.messages = this.mergeLocalMessages(latest) |
|
|
|
this.hasMoreMessages = page ? (page.current || 1) < (page.pages || 1) : false |
|
|
|
this.scrollToBottom(false) |
|
|
|
} finally { |
|
|
|
@ -664,6 +765,12 @@ |
|
|
|
if (!this.messages.length) return |
|
|
|
this.scrollAnimation = animated |
|
|
|
this.scrollIntoView = 'message-bottom-anchor' |
|
|
|
setTimeout(() => { |
|
|
|
this.scrollIntoView = '' |
|
|
|
this.$nextTick(() => { |
|
|
|
this.scrollIntoView = 'message-bottom-anchor' |
|
|
|
}) |
|
|
|
}, 80) |
|
|
|
}) |
|
|
|
}, |
|
|
|
sendPresence(text) { |
|
|
|
@ -679,11 +786,15 @@ |
|
|
|
this.voiceMode = false |
|
|
|
this.showEmoji = !this.showEmoji |
|
|
|
}, |
|
|
|
closeEmoji() { |
|
|
|
this.showEmoji = false |
|
|
|
}, |
|
|
|
toggleVoiceMode() { |
|
|
|
if (this.isBlocked) { |
|
|
|
uni.showToast({ title: '已加入黑名单,无法继续发送', icon: 'none' }) |
|
|
|
return |
|
|
|
} |
|
|
|
this.closeEmoji() |
|
|
|
this.voiceMode = !this.voiceMode |
|
|
|
// 切到语音模式就先把麦克风权限要好,避免按住说话时才失败 |
|
|
|
if (this.voiceMode) this.ensureRecordAuth() |
|
|
|
@ -787,8 +898,12 @@ |
|
|
|
pending: true, |
|
|
|
blocked: false, |
|
|
|
blockTip: '', |
|
|
|
localState: media.localState || 'sending', |
|
|
|
localOnly: true, |
|
|
|
createAt: Date.now(), |
|
|
|
mediaAuditStatus: media.mediaAuditStatus || 0 |
|
|
|
}) |
|
|
|
this.saveLocalMessages() |
|
|
|
this.scrollToBottom() |
|
|
|
return clientMsgId |
|
|
|
}, |
|
|
|
@ -797,7 +912,9 @@ |
|
|
|
if (!msg) return |
|
|
|
msg.pending = false |
|
|
|
msg.blocked = true |
|
|
|
msg.localState = 'failed' |
|
|
|
msg.blockTip = tip || '发送失败,请稍后再试' |
|
|
|
this.saveLocalMessages() |
|
|
|
}, |
|
|
|
canSendMedia() { |
|
|
|
if (this.isBlocked) { |
|
|
|
@ -834,17 +951,20 @@ |
|
|
|
(ack.messageId && String(item.messageId || '') === String(ack.messageId))) |
|
|
|
if (!msg) return |
|
|
|
const wasBlocked = !!msg.blocked |
|
|
|
msg.pending = false |
|
|
|
msg.pending = ack.mediaAuditStatus === 1 |
|
|
|
msg.messageId = ack.messageId |
|
|
|
msg.messageType = ack.messageType || msg.messageType || 1 |
|
|
|
msg.mediaDuration = ack.mediaDuration || msg.mediaDuration |
|
|
|
msg.mediaSize = ack.mediaSize || msg.mediaSize |
|
|
|
msg.mediaFormat = ack.mediaFormat || msg.mediaFormat |
|
|
|
msg.poster = ack.poster || msg.poster || '' |
|
|
|
msg.mediaAuditStatus = ack.mediaAuditStatus || msg.mediaAuditStatus || 0 |
|
|
|
msg.blocked = ack.isBlocked === 1 |
|
|
|
msg.localOnly = false |
|
|
|
msg.localState = '' |
|
|
|
if (msg.blocked) { |
|
|
|
if (msg.messageType === 1) msg.content = '这句话没有送出,请换一种更温柔的说法。' |
|
|
|
msg.blockTip = '内容未通过审核,请换一个更合适的内容' |
|
|
|
msg.blockTip = ack.auditTip || '内容未通过审核,请换一个更合适的内容' |
|
|
|
if (!wasBlocked) { |
|
|
|
uni.showToast({ title: '内容未通过审核,请换一个更合适的内容', icon: 'none' }) |
|
|
|
} |
|
|
|
@ -853,12 +973,14 @@ |
|
|
|
if (msg.messageType === 1 || msg.messageType === 3) msg.content = ack.content || msg.content |
|
|
|
this.loadStreak(true) |
|
|
|
} |
|
|
|
this.saveLocalMessages() |
|
|
|
}, |
|
|
|
chooseMedia() { |
|
|
|
if (this.isBlocked) { |
|
|
|
uni.showToast({ title: '已加入黑名单,无法继续发送', icon: 'none' }) |
|
|
|
return |
|
|
|
} |
|
|
|
this.closeEmoji() |
|
|
|
if (this.uploadingImage) return |
|
|
|
if (!uni.chooseMedia) { |
|
|
|
this.chooseImageCompat() |
|
|
|
@ -908,18 +1030,49 @@ |
|
|
|
const url = typeof result === 'string' ? result : (result && (result.url || result.fileUrl || result.path || result.fullPath)) |
|
|
|
return url && typeof url === 'string' ? url : '' |
|
|
|
}, |
|
|
|
silentUpload(filePath) { |
|
|
|
return new Promise((resolve, reject) => { |
|
|
|
uni.uploadFile({ |
|
|
|
url: tui.interfaceUrl() + '/upload/file', |
|
|
|
filePath, |
|
|
|
name: 'file', |
|
|
|
timeout: 300000, |
|
|
|
header: { accessToken: uni.getStorageSync('hiver_token') }, |
|
|
|
success: (res) => { |
|
|
|
try { |
|
|
|
const d = JSON.parse(String(res.data || '{}').replace(/\ufeff/g, '')) |
|
|
|
const fileObj = d.result !== undefined ? d.result : d.data |
|
|
|
const url = this.extractUploadUrl(fileObj) |
|
|
|
if (d.code % 100 === 0 && url) { |
|
|
|
resolve(url) |
|
|
|
return |
|
|
|
} |
|
|
|
reject(new Error(d.message || '上传失败')) |
|
|
|
} catch (e) { |
|
|
|
reject(e) |
|
|
|
} |
|
|
|
}, |
|
|
|
fail: (err) => reject(new Error((err && err.errMsg) || '上传失败,请检查网络')) |
|
|
|
}) |
|
|
|
}) |
|
|
|
}, |
|
|
|
async uploadAndSendImage(filePath, fileSize) { |
|
|
|
if (!this.canSendMedia()) return |
|
|
|
this.showEmoji = false |
|
|
|
// 本地临时路径先上屏,后台再上传+送审 |
|
|
|
const clientMsgId = this.pushLocalMessage(filePath, 2, { mediaSize: fileSize, mediaFormat: 'image' }) |
|
|
|
const clientMsgId = this.pushLocalMessage(filePath, 2, { mediaSize: fileSize, mediaFormat: 'image', localState: 'uploading' }) |
|
|
|
this.uploadingImage = true |
|
|
|
try { |
|
|
|
const imageUrl = this.extractUploadUrl(await tui.uploadFile('/upload/file', filePath)) |
|
|
|
const imageUrl = await this.silentUpload(filePath) |
|
|
|
if (!imageUrl) { |
|
|
|
this.markSendFailed(clientMsgId, '图片上传失败,请稍后再试') |
|
|
|
return |
|
|
|
} |
|
|
|
const local = this.messages.find(item => item.clientMsgId === clientMsgId) |
|
|
|
if (local) { |
|
|
|
local.localState = 'sending' |
|
|
|
this.saveLocalMessages() |
|
|
|
} |
|
|
|
this.persistMessage({ |
|
|
|
roomId: Number(this.roomId), |
|
|
|
clientMsgId, |
|
|
|
@ -944,11 +1097,12 @@ |
|
|
|
mediaDuration, |
|
|
|
mediaSize: file.size, |
|
|
|
mediaFormat: 'video', |
|
|
|
poster: file.thumbTempFilePath || '' |
|
|
|
poster: file.thumbTempFilePath || '', |
|
|
|
localState: 'uploading' |
|
|
|
}) |
|
|
|
this.uploadingImage = true |
|
|
|
try { |
|
|
|
const videoUrl = this.extractUploadUrl(await tui.uploadFile('/upload/file', file.tempFilePath)) |
|
|
|
const videoUrl = await this.silentUpload(file.tempFilePath) |
|
|
|
if (!videoUrl) { |
|
|
|
this.markSendFailed(clientMsgId, '视频上传失败,请稍后再试') |
|
|
|
return |
|
|
|
@ -956,11 +1110,16 @@ |
|
|
|
let posterUrl = '' |
|
|
|
if (file.thumbTempFilePath) { |
|
|
|
try { |
|
|
|
posterUrl = this.extractUploadUrl(await tui.uploadFile('/upload/file', file.thumbTempFilePath)) |
|
|
|
posterUrl = await this.silentUpload(file.thumbTempFilePath) |
|
|
|
} catch (e) { |
|
|
|
posterUrl = '' |
|
|
|
} |
|
|
|
} |
|
|
|
const local = this.messages.find(item => item.clientMsgId === clientMsgId) |
|
|
|
if (local) { |
|
|
|
local.localState = 'sending' |
|
|
|
this.saveLocalMessages() |
|
|
|
} |
|
|
|
this.persistMessage({ |
|
|
|
roomId: Number(this.roomId), |
|
|
|
clientMsgId, |
|
|
|
@ -1030,6 +1189,13 @@ |
|
|
|
this.ensureRecordAuth() |
|
|
|
}) |
|
|
|
}, |
|
|
|
vibrateOnRecordStart() { |
|
|
|
try { |
|
|
|
if (uni.vibrateShort) { |
|
|
|
uni.vibrateShort({ type: 'light' }) |
|
|
|
} |
|
|
|
} catch (e) {} |
|
|
|
}, |
|
|
|
initAudio() { |
|
|
|
if (!uni.createInnerAudioContext) return |
|
|
|
this.innerAudioContext = uni.createInnerAudioContext() |
|
|
|
@ -1067,6 +1233,7 @@ |
|
|
|
this.pendingStop = false |
|
|
|
return |
|
|
|
} |
|
|
|
this.vibrateOnRecordStart() |
|
|
|
this.recorderManager.start({ |
|
|
|
duration: 60000, |
|
|
|
sampleRate: 16000, |
|
|
|
@ -1194,10 +1361,11 @@ |
|
|
|
if (!this.roomId) return |
|
|
|
const profile = await getIeRoomTargetProfile(this.roomId) |
|
|
|
if (!profile) return |
|
|
|
if (profile.userId) this.targetUserId = profile.userId |
|
|
|
const roomTargetUserId = this.targetUserId || profile.userId || '' |
|
|
|
const usableProfile = profile.exists !== false |
|
|
|
this.targetProfile = usableProfile ? { |
|
|
|
...profile, |
|
|
|
userId: roomTargetUserId, |
|
|
|
anonymousName: profile.anonymousName || this.companion.name, |
|
|
|
avatarText: profile.avatarText || this.companion.avatar, |
|
|
|
avatarUrl: profile.avatarUrl || this.companion.avatarUrl, |
|
|
|
@ -1207,6 +1375,7 @@ |
|
|
|
personaImages: profile.personaImages || [] |
|
|
|
} : { |
|
|
|
...profile, |
|
|
|
userId: roomTargetUserId, |
|
|
|
anonymousName: this.companion.name, |
|
|
|
avatarText: this.companion.avatar, |
|
|
|
avatarUrl: this.companion.avatarUrl, |
|
|
|
@ -1231,17 +1400,31 @@ |
|
|
|
this.showReportPanel = true |
|
|
|
}, |
|
|
|
async submitReport() { |
|
|
|
if (!this.roomId || !this.targetUserId) { |
|
|
|
uni.showToast({ title: '举报对象缺失', icon: 'none' }) |
|
|
|
if (this.reporting) return |
|
|
|
if (!this.roomId) { |
|
|
|
uni.showToast({ title: '房间信息缺失', icon: 'none' }) |
|
|
|
return |
|
|
|
} |
|
|
|
await reportIeRoom(this.roomId, { |
|
|
|
reportedUserId: this.targetUserId, |
|
|
|
reasonType: this.reportForm.reasonType, |
|
|
|
reasonText: this.reportForm.reasonText || '聊天内容不适' |
|
|
|
}) |
|
|
|
this.showReportPanel = false |
|
|
|
uni.showToast({ title: '已收到举报', icon: 'none' }) |
|
|
|
this.reporting = true |
|
|
|
try { |
|
|
|
await reportIeRoom(this.roomId, { |
|
|
|
reasonType: this.reportForm.reasonType, |
|
|
|
reasonText: this.reportForm.reasonText || '聊天内容不适', |
|
|
|
...this.getAreaInfo() |
|
|
|
}) |
|
|
|
this.showReportPanel = false |
|
|
|
uni.showToast({ title: '已提交举报,平台会尽快处理', icon: 'none' }) |
|
|
|
} finally { |
|
|
|
this.reporting = false |
|
|
|
} |
|
|
|
}, |
|
|
|
getAreaInfo() { |
|
|
|
try { |
|
|
|
const area = JSON.parse(uni.getStorageSync('area') || '{}') |
|
|
|
return { regionId: area.id || '', regionName: area.title || '' } |
|
|
|
} catch (e) { |
|
|
|
return { regionId: '', regionName: '' } |
|
|
|
} |
|
|
|
}, |
|
|
|
toggleBlock() { |
|
|
|
if (this.blockedByMe) { |
|
|
|
@ -1326,6 +1509,7 @@ |
|
|
|
mediaDuration: msg.mediaDuration, |
|
|
|
mediaSize: msg.mediaSize, |
|
|
|
mediaFormat: msg.mediaFormat, |
|
|
|
poster: msg.poster || '', |
|
|
|
mediaAuditStatus: msg.mediaAuditStatus || 0, |
|
|
|
content: msg.content, |
|
|
|
mine: String(msg.senderId || '') === String(uni.getStorageSync('id') || '') |
|
|
|
@ -1660,6 +1844,12 @@ |
|
|
|
border: 1rpx solid rgba(255, 138, 84, 0.3); |
|
|
|
} |
|
|
|
|
|
|
|
.timer-pill.zero { |
|
|
|
color: rgba(22, 27, 46, .38); |
|
|
|
background: rgba(22, 27, 46, .05); |
|
|
|
border: 1rpx solid rgba(22, 27, 46, .06); |
|
|
|
} |
|
|
|
|
|
|
|
.timer-pill.expiring { |
|
|
|
animation: streakBlink 1.2s ease-in-out infinite; |
|
|
|
} |
|
|
|
@ -1687,11 +1877,6 @@ |
|
|
|
box-sizing: border-box; |
|
|
|
} |
|
|
|
|
|
|
|
.message-bottom-spacer { |
|
|
|
width: 100%; |
|
|
|
flex-shrink: 0; |
|
|
|
} |
|
|
|
|
|
|
|
.load-more { |
|
|
|
width: 360rpx; |
|
|
|
margin: 0 auto 24rpx; |
|
|
|
@ -1797,17 +1982,22 @@ |
|
|
|
filter: grayscale(1); |
|
|
|
} |
|
|
|
|
|
|
|
.blocked-tip { |
|
|
|
.message-status { |
|
|
|
margin-top: 10rpx; |
|
|
|
padding: 8rpx 18rpx; |
|
|
|
border-radius: 999rpx; |
|
|
|
color: #e85d75; |
|
|
|
background: rgba(255, 228, 236, .82); |
|
|
|
color: #9a6b18; |
|
|
|
background: rgba(255, 196, 87, 0.16); |
|
|
|
font-size: 20rpx; |
|
|
|
line-height: 30rpx; |
|
|
|
max-width: 460rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.message-status.blocked { |
|
|
|
color: #e85d75; |
|
|
|
background: rgba(255, 228, 236, .82); |
|
|
|
} |
|
|
|
|
|
|
|
.image-bubble { |
|
|
|
padding: 8rpx; |
|
|
|
background: rgba(255, 255, 255, 0.72); |
|
|
|
@ -1901,7 +2091,8 @@ |
|
|
|
} |
|
|
|
|
|
|
|
.voice-bubble { |
|
|
|
min-width: 180rpx; |
|
|
|
min-width: 170rpx; |
|
|
|
max-width: 400rpx; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: space-between; |
|
|
|
@ -1923,7 +2114,7 @@ |
|
|
|
left: 28rpx; |
|
|
|
right: 28rpx; |
|
|
|
bottom: 132rpx; |
|
|
|
z-index: 13; |
|
|
|
z-index: 12; |
|
|
|
display: flex; |
|
|
|
overflow-x: auto; |
|
|
|
white-space: nowrap; |
|
|
|
@ -1957,7 +2148,7 @@ |
|
|
|
.quiz-panel { |
|
|
|
width: 100%; |
|
|
|
max-height: 76vh; |
|
|
|
padding: 32rpx 30rpx 30rpx; |
|
|
|
padding: 32rpx 30rpx 40rpx; |
|
|
|
border-radius: 38rpx; |
|
|
|
background: linear-gradient(180deg, #ffffff 0%, #f6f8ff 100%); |
|
|
|
box-sizing: border-box; |
|
|
|
@ -2000,7 +2191,7 @@ |
|
|
|
|
|
|
|
.quiz-scroll { |
|
|
|
margin-top: 20rpx; |
|
|
|
max-height: 46vh; |
|
|
|
max-height: 42vh; |
|
|
|
} |
|
|
|
|
|
|
|
.quiz-q { |
|
|
|
@ -2083,7 +2274,7 @@ |
|
|
|
left: 24rpx; |
|
|
|
right: 24rpx; |
|
|
|
bottom: 30rpx; |
|
|
|
z-index: 14; |
|
|
|
z-index: 30; |
|
|
|
height: 88rpx; |
|
|
|
padding: 10rpx; |
|
|
|
box-sizing: border-box; |
|
|
|
@ -2100,24 +2291,48 @@ |
|
|
|
left: 24rpx; |
|
|
|
right: 24rpx; |
|
|
|
bottom: 128rpx; |
|
|
|
z-index: 11; |
|
|
|
display: flex; |
|
|
|
flex-wrap: wrap; |
|
|
|
padding: 22rpx 20rpx 12rpx; |
|
|
|
z-index: 31; |
|
|
|
max-height: 286rpx; |
|
|
|
padding: 18rpx 18rpx 14rpx; |
|
|
|
border: 1rpx solid rgba(255, 255, 255, 0.78); |
|
|
|
border-radius: 34rpx; |
|
|
|
background: rgba(255, 255, 255, 0.88); |
|
|
|
backdrop-filter: blur(20rpx); |
|
|
|
box-shadow: 0 18rpx 58rpx rgba(96, 112, 160, 0.16); |
|
|
|
box-sizing: border-box; |
|
|
|
overflow: hidden; |
|
|
|
} |
|
|
|
|
|
|
|
.emoji-close-mask { |
|
|
|
position: fixed; |
|
|
|
inset: 0; |
|
|
|
z-index: 29; |
|
|
|
background: transparent; |
|
|
|
} |
|
|
|
|
|
|
|
.emoji-scroll { |
|
|
|
max-height: 254rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.emoji-grid { |
|
|
|
display: flex; |
|
|
|
flex-wrap: wrap; |
|
|
|
padding-bottom: 4rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.emoji-item { |
|
|
|
width: 16.66%; |
|
|
|
height: 66rpx; |
|
|
|
width: 14.28%; |
|
|
|
height: 64rpx; |
|
|
|
margin-bottom: 8rpx; |
|
|
|
border-radius: 22rpx; |
|
|
|
text-align: center; |
|
|
|
line-height: 66rpx; |
|
|
|
font-size: 42rpx; |
|
|
|
line-height: 64rpx; |
|
|
|
font-size: 40rpx; |
|
|
|
} |
|
|
|
|
|
|
|
.emoji-item:active { |
|
|
|
background: rgba(169, 255, 231, .34); |
|
|
|
transform: scale(.92); |
|
|
|
} |
|
|
|
|
|
|
|
.input { |
|
|
|
@ -2154,7 +2369,8 @@ |
|
|
|
} |
|
|
|
|
|
|
|
.tool-btn { |
|
|
|
width: 62rpx; |
|
|
|
flex-shrink: 0; |
|
|
|
width: 66rpx; |
|
|
|
height: 62rpx; |
|
|
|
border-radius: 50%; |
|
|
|
text-align: center; |
|
|
|
@ -2165,6 +2381,14 @@ |
|
|
|
font-weight: 800; |
|
|
|
} |
|
|
|
|
|
|
|
.voice-toggle { |
|
|
|
width: 78rpx; |
|
|
|
margin: 0 8rpx; |
|
|
|
border-radius: 999rpx; |
|
|
|
font-size: 34rpx; |
|
|
|
background: rgba(169, 255, 231, .5); |
|
|
|
} |
|
|
|
|
|
|
|
.send { |
|
|
|
height: 66rpx; |
|
|
|
line-height: 66rpx; |
|
|
|
@ -2187,7 +2411,7 @@ |
|
|
|
right: 0; |
|
|
|
top: 0; |
|
|
|
bottom: 0; |
|
|
|
z-index: 20; |
|
|
|
z-index: 40; |
|
|
|
display: flex; |
|
|
|
align-items: flex-end; |
|
|
|
padding: 28rpx; |
|
|
|
|