mirror of
https://github.com/RetroShare/RSNewWebUI.git
synced 2026-09-13 18:45:49 +05:00
Every JSON API answer carries `Connection: close` and the server runs its service on a single thread, so each request costs a full TCP handshake and they are answered one at a time. Over loopback a round trip is 0.1 ms and none of this shows; over WiFi or 4G it is 20 to 80 ms and the three patterns below turn into seconds of blank screen. Chat room list: one getChatLobbyInfo per subscribed room, and nothing was painted until the last answer arrived -- then only did the public room list get asked for. Paint each room as it lands and start the public list immediately. Forum post bodies: loadPostContent() is called from the view, and the body stays null for the whole round trip, so every redraw fired the same getForumContent again -- and each of the other posts' answers causes a redraw. An open thread multiplied one request per post into one per post per redraw. Guarded by an in-flight set, kept on failure so an unavailable post is asked for once per visit rather than forever. Chat history preload: two getMessages per known identity, hundreds of them at once, filling the browser's six sockets and the single service thread while the user waits for something else. Run four at a time. The duplicated response handling of the two branches is now one function.
298 lines
9.3 KiB
JavaScript
298 lines
9.3 KiB
JavaScript
const m = require('mithril');
|
|
const rs = require('rswebui');
|
|
const widget = require('widgets');
|
|
|
|
const GROUP_SUBSCRIBE_ADMIN = 0x01; // means: you have the admin key for this group
|
|
const GROUP_SUBSCRIBE_PUBLISH = 0x02; // means: you have the publish key for thiss group. Typical use: publish key in forums are shared with specific friends.
|
|
const GROUP_SUBSCRIBE_SUBSCRIBED = 0x04; // means: you are subscribed to a group, which makes you a source for this group to your friend nodes.
|
|
const GROUP_SUBSCRIBE_NOT_SUBSCRIBED = 0x08;
|
|
const GROUP_MY_FORUM = GROUP_SUBSCRIBE_ADMIN + GROUP_SUBSCRIBE_SUBSCRIBED + GROUP_SUBSCRIBE_PUBLISH;
|
|
|
|
const THREAD_UNREAD = 0x00000003;
|
|
|
|
const Data = {
|
|
DisplayForums: {},
|
|
Threads: {},
|
|
ParentThreads: {},
|
|
ParentThreadMap: {},
|
|
loading: new Set(),
|
|
};
|
|
|
|
// 'forumId/msgId' of the post bodies currently being fetched, see
|
|
// loadPostContent(). Module level rather than in Data: it is plumbing, not
|
|
// forum content.
|
|
const bodyRequestsInFlight = new Set();
|
|
|
|
function getTimestampValue(ts) {
|
|
if (!ts) return 0;
|
|
if (typeof ts === 'object') {
|
|
if (ts.xint64 !== undefined) return ts.xint64;
|
|
if (ts.xstr64 !== undefined) return Number(ts.xstr64);
|
|
return 0;
|
|
}
|
|
return ts;
|
|
}
|
|
|
|
function formatTimestamp(ts) {
|
|
const val = getTimestampValue(ts);
|
|
if (!val || val === 0) return '???';
|
|
try {
|
|
const localDate = new Date(val * 1000);
|
|
const offset = localDate.getTimezoneOffset() * 60000;
|
|
return new Date(localDate.getTime() - offset).toISOString().replace('T', ' ').slice(0, 16);
|
|
} catch (e) {
|
|
return 'Invalid Date';
|
|
}
|
|
}
|
|
|
|
async function updatedisplayforums(keyid) {
|
|
if (Data.loading.has(keyid)) return;
|
|
Data.loading.add(keyid);
|
|
|
|
try {
|
|
const res1 = await rs.rsJsonApiRequest('/rsgxsforums/getForumsInfo', {
|
|
forumIds: [keyid], // keyid: Forumid
|
|
});
|
|
if (res1 && res1.body && res1.body.retval && res1.body.forumsInfo && res1.body.forumsInfo.length > 0) {
|
|
const forumInfo = res1.body.forumsInfo[0];
|
|
Data.DisplayForums[keyid] = {
|
|
// struct for a forum
|
|
name: forumInfo.mMeta.mGroupName,
|
|
author: forumInfo.mMeta.mAuthorId,
|
|
isSearched: true,
|
|
description: forumInfo.mDescription,
|
|
isSubscribed:
|
|
forumInfo.mMeta.mSubscribeFlags === GROUP_SUBSCRIBE_SUBSCRIBED ||
|
|
forumInfo.mMeta.mSubscribeFlags === GROUP_MY_FORUM,
|
|
activity: forumInfo.mMeta.mLastPost,
|
|
created: forumInfo.mMeta.mPublishTs,
|
|
};
|
|
if (Data.Threads[keyid] === undefined) {
|
|
Data.Threads[keyid] = {};
|
|
}
|
|
|
|
const res2 = await rs.rsJsonApiRequest('/rsgxsforums/getForumPostsHierarchy', {
|
|
group: forumInfo,
|
|
});
|
|
|
|
if (res2 && res2.body && res2.body.vect) {
|
|
const vect = res2.body.vect;
|
|
// Index 0 is the root sentinel in GXS hierarchy
|
|
const rootSentinel = vect[0];
|
|
|
|
if (rootSentinel && rootSentinel.mChildren) {
|
|
Data.ParentThreads[keyid] = {};
|
|
rootSentinel.mChildren.forEach((topIndex) => {
|
|
const EntryToThread = (entryIndex) => {
|
|
const entry = vect[entryIndex];
|
|
const replies = {};
|
|
|
|
// Map ForumPostEntry to a structure compatible with the existing UI
|
|
const meta = {
|
|
mGroupId: keyid,
|
|
mMsgId: entry.mMsgId,
|
|
mOrigMsgId: entry.mMsgId,
|
|
mThreadId: entry.mMsgId,
|
|
mParentId:
|
|
entry.mParent !== 0
|
|
? vect[entry.mParent].mMsgId
|
|
: '00000000000000000000000000000000',
|
|
mAuthorId: entry.mAuthorId,
|
|
mMsgName: entry.mTitle,
|
|
mPublishTs: entry.mPublishTs,
|
|
mMostRecentTsInThread: getTimestampValue(entry.mPublishTs),
|
|
mMsgStatus: entry.mMsgStatus,
|
|
};
|
|
|
|
// Populate ParentThreadMap for compatibility
|
|
if (meta.mParentId !== '00000000000000000000000000000000') {
|
|
if (!Data.ParentThreadMap[meta.mParentId]) Data.ParentThreadMap[meta.mParentId] = {};
|
|
Data.ParentThreadMap[meta.mParentId][meta.mMsgId] = meta;
|
|
}
|
|
|
|
const threadStruct = {
|
|
thread: { mMeta: meta, mMsg: null },
|
|
replies,
|
|
showReplies: false,
|
|
};
|
|
|
|
// Add to flat map
|
|
Data.Threads[keyid][meta.mMsgId] = threadStruct;
|
|
|
|
if (entry.mChildren) {
|
|
entry.mChildren.forEach((childIndex) => {
|
|
const childThread = EntryToThread(childIndex);
|
|
replies[childThread.thread.mMeta.mMsgId] = childThread;
|
|
const childTs = childThread.thread.mMeta.mMostRecentTsInThread || 0;
|
|
if (childTs > meta.mMostRecentTsInThread) meta.mMostRecentTsInThread = childTs;
|
|
});
|
|
}
|
|
|
|
return threadStruct;
|
|
};
|
|
|
|
const topThread = EntryToThread(topIndex);
|
|
Data.ParentThreads[keyid][topThread.thread.mMeta.mMsgId] = topThread.thread.mMeta;
|
|
});
|
|
}
|
|
}
|
|
m.redraw();
|
|
}
|
|
} catch (e) {
|
|
console.error('[RS] Error updating forum display for:', keyid, e);
|
|
} finally {
|
|
Data.loading.delete(keyid);
|
|
m.redraw(); // Final redraw just in case
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load the body (mMsg) of a single forum post on demand.
|
|
* Returns a Promise that resolves to the body string, or null on failure.
|
|
*/
|
|
async function loadPostContent(forumId, msgId) {
|
|
// If body is already loaded, return it immediately
|
|
if (
|
|
Data.Threads[forumId] &&
|
|
Data.Threads[forumId][msgId] &&
|
|
Data.Threads[forumId][msgId].thread.mMsg !== null
|
|
) {
|
|
return Data.Threads[forumId][msgId].thread.mMsg;
|
|
}
|
|
|
|
// This is called straight from the view (forum_view.js, the 'Loading
|
|
// content...' branch), and the body stays null for the whole round trip, so
|
|
// without this guard every redraw fires another getForumContent for the same
|
|
// post -- and a redraw happens on each of the other posts' answers. An open
|
|
// thread would multiply one request per post into one per post per redraw.
|
|
const inFlightKey = forumId + '/' + msgId;
|
|
if (bodyRequestsInFlight.has(inFlightKey)) return null;
|
|
bodyRequestsInFlight.add(inFlightKey);
|
|
|
|
try {
|
|
const res = await rs.rsJsonApiRequest('/rsgxsforums/getForumContent', {
|
|
forumId,
|
|
msgsIds: [msgId],
|
|
});
|
|
if (res && res.body && res.body.retval && res.body.msgs && res.body.msgs.length > 0) {
|
|
const body = res.body.msgs[0].mMsg;
|
|
// Cache the body in the existing thread entry
|
|
if (Data.Threads[forumId] && Data.Threads[forumId][msgId]) {
|
|
Data.Threads[forumId][msgId].thread.mMsg = body;
|
|
}
|
|
// The cached body is what stops the view from asking again, so the key
|
|
// is only released once it is in place.
|
|
bodyRequestsInFlight.delete(inFlightKey);
|
|
m.redraw();
|
|
return body;
|
|
}
|
|
} catch (e) {
|
|
console.error('[RS] Error loading post content:', forumId, msgId, e);
|
|
}
|
|
// Failure: the key is deliberately kept, so a post the core cannot return
|
|
// is asked for once per visit instead of once per redraw, forever.
|
|
return null;
|
|
}
|
|
|
|
const DisplayForumsFromList = () => {
|
|
return {
|
|
view: (v) =>
|
|
m(
|
|
'tr',
|
|
{
|
|
key: v.attrs.id,
|
|
class:
|
|
Data.DisplayForums[v.attrs.id] && Data.DisplayForums[v.attrs.id].isSearched
|
|
? ''
|
|
: 'hidden',
|
|
onclick: () => {
|
|
m.route.set('/forums/:tab/:mGroupId', {
|
|
tab: v.attrs.category,
|
|
mGroupId: v.attrs.id,
|
|
});
|
|
},
|
|
},
|
|
[m('td', Data.DisplayForums[v.attrs.id] ? Data.DisplayForums[v.attrs.id].name : '')]
|
|
),
|
|
};
|
|
};
|
|
|
|
const ForumSummary = () => {
|
|
let keyid = {};
|
|
return {
|
|
oninit: (v) => {
|
|
keyid = v.attrs.details.mGroupId;
|
|
updatedisplayforums(keyid);
|
|
},
|
|
view: (v) => { },
|
|
};
|
|
};
|
|
|
|
const ForumTable = () => {
|
|
return {
|
|
view: (v) => m('table.forums', [m('tr', [m('th', 'Forum Name')]), v.children]),
|
|
};
|
|
};
|
|
const ThreadsTable = () => {
|
|
return {
|
|
oninit: (v) => { },
|
|
view: (v) =>
|
|
m('table.threads', [
|
|
v.children,
|
|
]),
|
|
};
|
|
};
|
|
const ThreadsReplyTable = () => {
|
|
return {
|
|
oninit: (v) => { },
|
|
view: (v) =>
|
|
m('table.threadreply', [
|
|
v.children,
|
|
]),
|
|
};
|
|
};
|
|
|
|
const SearchBar = () => {
|
|
let searchString = '';
|
|
return {
|
|
view: (v) =>
|
|
m('input[type=text][id=searchforum][placeholder=Search Subject].searchbar', {
|
|
value: searchString,
|
|
oninput: (e) => {
|
|
searchString = e.target.value.toLowerCase();
|
|
for (const hash in Data.DisplayForums) {
|
|
if (Data.DisplayForums[hash].name.toLowerCase().indexOf(searchString) > -1) {
|
|
Data.DisplayForums[hash].isSearched = true;
|
|
} else {
|
|
Data.DisplayForums[hash].isSearched = false;
|
|
}
|
|
}
|
|
},
|
|
}),
|
|
};
|
|
};
|
|
function popupmessage(message, modalClass = '') {
|
|
widget.popupMessage(message, modalClass);
|
|
}
|
|
|
|
module.exports = {
|
|
Data,
|
|
SearchBar,
|
|
ForumSummary,
|
|
DisplayForumsFromList,
|
|
ForumTable,
|
|
ThreadsTable,
|
|
ThreadsReplyTable,
|
|
popupmessage,
|
|
updatedisplayforums,
|
|
loadPostContent,
|
|
getTimestampValue,
|
|
formatTimestamp,
|
|
GROUP_SUBSCRIBE_ADMIN,
|
|
GROUP_SUBSCRIBE_NOT_SUBSCRIBED,
|
|
GROUP_SUBSCRIBE_PUBLISH,
|
|
GROUP_SUBSCRIBE_SUBSCRIBED,
|
|
GROUP_MY_FORUM,
|
|
THREAD_UNREAD,
|
|
};
|