chat: invite-ссылки с выбором достижимого узла + tcp_io фикс half-close FIN
- Общий модуль invite_build: сборка utun://-ссылок, авто-выбор лучшего достижимого узла, список кандидатов (вынесено из android jni_bridge) - chatgui: диалог invite с выбором узла (комбобокс), события INVITE_CANDIDATES / INVITE_LINK_READY через gui_bridge - headless/chatcli: invite <ch_id> [node_id], команда invite_nodes - tcp_io: error_cb отличает грациозный EPOLLHUP (SO_ERROR==0) от реальной ошибки — при приостановленном чтении FIN не роняет недослейные данные (фикс flaky half-close) - run_test.sh: half-close echo на выделенном порту 19190 + отдельный DNAT (был недостижим)
This commit is contained in:
1 parent
a17a85a383
commit
1345f44e10
23 files changed
+872
-539
No files matched your search
@@ -488,40 +488,6 @@ void MessageList::refresh() {
|
||||
m_readDebounce->start();
|
||||
}
|
||||
|
||||
void MessageList::addMessage(const QString& channelId, quint64 authorNodeId,
|
||||
const QByteArray& content, qint64 timestamp) {
|
||||
if (channelId != m_currentChannelId) return;
|
||||
|
||||
AccountRow acc = m_db ? m_db->getAccount(authorNodeId) : AccountRow();
|
||||
QString author = m_db ? authorName(authorNodeId) : QString("node_%1").arg(authorNodeId);
|
||||
QString letter = acc.avatarLetter.isEmpty() ? author.mid(0, 1) : acc.avatarLetter;
|
||||
QColor color(acc.avatarColor);
|
||||
auto avatar = makeAvatar(color, letter);
|
||||
auto time = QDateTime::fromMSecsSinceEpoch(timestamp).toString("HH:mm");
|
||||
|
||||
/* dedup */
|
||||
for (int r = 0; r < m_model->rowCount(); r++) {
|
||||
auto* it = m_model->item(r);
|
||||
if (it->data(MsgTimeRole).toString() == time
|
||||
&& it->data(MsgAuthorRole).toString() == author) return;
|
||||
}
|
||||
|
||||
auto *item = new QStandardItem();
|
||||
setMsg(item, author, QString::fromUtf8(content), time, avatar, false,
|
||||
false, QString(), QString(), QVariantList{});
|
||||
bool isSelf = m_db && (authorNodeId == m_db->myNodeId());
|
||||
item->setData(QVariant::fromValue(authorNodeId), MsgAuthorNodeIdRole);
|
||||
item->setData(isSelf ? 1 : 0, MsgIsOutgoingRole);
|
||||
setVoiceMessageRoles(item, content, QString(),
|
||||
m_db ? m_db->dbPath() + "/media" : QString(), m_currentChannelId, QByteArray());
|
||||
m_model->appendRow(item);
|
||||
|
||||
if (m_model->rowCount() > 50)
|
||||
m_model->removeRow(0);
|
||||
|
||||
m_view->scrollToBottom();
|
||||
}
|
||||
|
||||
void MessageList::saveChannelState() {
|
||||
if (m_currentChannelId.isEmpty() || m_model->rowCount() == 0) return;
|
||||
|
||||
@@ -679,44 +645,36 @@ void MessageList::updateMessageProgress(const QString& chId, int64_t msgId,
|
||||
}
|
||||
|
||||
void MessageList::onAttachmentDownloaded(const QString& chId, int64_t msgId) {
|
||||
QString ct;
|
||||
QString filePath;
|
||||
if (chId != m_currentChannelId || !m_db || !m_db->isOpen()) return;
|
||||
|
||||
MessageRow m = m_db->getMessageById(chId, msgId);
|
||||
if (m.id == 0) return;
|
||||
|
||||
int row = -1;
|
||||
bool wasPending = false;
|
||||
for (int r = 0; r < m_model->rowCount(); r++) {
|
||||
QStandardItem* item = m_model->item(r);
|
||||
if (!item) continue;
|
||||
if (item->data(MsgIdRole).toLongLong() == msgId) {
|
||||
QStandardItem* it = m_model->item(r);
|
||||
if (it && it->data(MsgIdRole).toLongLong() == msgId) {
|
||||
row = r;
|
||||
item->setData(2, MsgFileDownloadStateRole);
|
||||
item->setData(0, MsgMediaBlocksDoneRole);
|
||||
ct = item->data(MsgContentTypeRole).toString();
|
||||
filePath = item->data(MsgVoiceFileRole).toString();
|
||||
wasPending = it->data(MsgVoiceDownloadPendingRole).toBool();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (row >= 0 && chId == m_currentChannelId) {
|
||||
bool wasPending = false;
|
||||
for (int r = 0; r < m_model->rowCount(); r++) {
|
||||
QStandardItem* it = m_model->item(r);
|
||||
if (it && it->data(MsgIdRole).toLongLong() == msgId) {
|
||||
wasPending = it->data(MsgVoiceDownloadPendingRole).toBool();
|
||||
if (wasPending) it->setData(false, MsgVoiceDownloadPendingRole);
|
||||
break;
|
||||
}
|
||||
}
|
||||
refresh(); /* reload to get updated local_attrs with file path */
|
||||
/* auto-play only if user clicked play to initiate download */
|
||||
if (ct == "audio/opus" && wasPending) {
|
||||
QTimer::singleShot(500, this, [this, msgId]() {
|
||||
for (int r = 0; r < m_model->rowCount(); r++) {
|
||||
QStandardItem* it = m_model->item(r);
|
||||
if (it && it->data(MsgIdRole).toLongLong() == msgId) {
|
||||
QString fp = it->data(MsgVoiceFileRole).toString();
|
||||
if (!fp.isEmpty() && QFile::exists(fp))
|
||||
VoicePlayer::playOpusFile(fp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (row < 0) return;
|
||||
|
||||
QStandardItem* item = m_model->item(row);
|
||||
setVoiceMessageRoles(item, m.data, m.contentType,
|
||||
m_db->dbPath() + "/media", chId, m.localAttrs);
|
||||
item->setData(0, MsgMediaBlocksDoneRole);
|
||||
item->setData(false, MsgVoiceDownloadPendingRole);
|
||||
|
||||
if (m.contentType == "audio/opus" && wasPending) {
|
||||
QString fp = item->data(MsgVoiceFileRole).toString();
|
||||
if (!fp.isEmpty() && QFile::exists(fp)) {
|
||||
QTimer::singleShot(500, this, [this, fp]() {
|
||||
MessageDelegate::stopAllAutoPlay();
|
||||
float actualDur = VoicePlayer::playOpusFile(fp);
|
||||
MessageDelegate::startAutoPlay(fp, actualDur);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user