Improved Channels for Phone & Desktop

Fixed channels thumbnail layout issue
Added same comments design from boards to channels
This commit is contained in:
defnax 2026-07-30 23:56:39 +02:00
parent 04d42dc38f
commit 709c0022f0
9 changed files with 6559 additions and 167 deletions

View File

@ -13,24 +13,15 @@ function numberValue(value) {
* Fallback SVG Thumbnail when no image is available
*/
const FallbackImage = () =>
m(
'svg.board-card__placeholder-img',
{
xmlns: 'http://www.w3.org/2000/svg',
viewBox: '0 0 24 24',
fill: 'none',
stroke: 'currentColor',
strokeWidth: '1.5',
strokeLinecap: 'round',
strokeLinejoin: 'round',
'aria-hidden': 'true',
m('.board-card__placeholder-content', {
style: {
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
gap: '.3rem', color: '#64748b', fontSize: '.72rem', fontWeight: '600', textAlign: 'center',
},
[
m('rect', { x: '3', y: '3', width: '18', height: '18', rx: '2', ry: '2' }),
m('circle', { cx: '8.5', cy: '8.5', r: '1.5' }),
m('polyline', { points: '21 15 16 10 5 21' }),
]
);
}, [
m('i.fas.fa-image[aria-hidden=true]', { style: { fontSize: '1.35rem' } }),
m('span', 'No image'),
]);
/**
* Check if notes string contains non-whitespace text

View File

@ -3,6 +3,7 @@ const util = require('boards/boards_util');
const boardKanban = require('boards/board_kanban');
const rs = require('rswebui');
const peopleUtil = require('people/people_util');
const chatEmoji = require('chat/chat_emoji');
const Data = util.Data;
function createboard() {
@ -275,10 +276,13 @@ function PostView() {
let loadingComments = true;
let identities = [];
let authorId = null;
let voteIdentity = null;
let replyTo = null;
let composerText = '';
let submitting = false;
let submitError = '';
let notesExpanded = false;
let showEmojiPicker = false;
const expandedReplies = {};
const metaOf = (comment) => (comment && comment.mMeta) || {};
@ -372,6 +376,7 @@ function PostView() {
peopleUtil.ownIds((ids) => {
identities = (ids || []).filter((id) => Number(id) !== 0);
authorId = identities[0] || null;
voteIdentity = identities[0] || null;
m.redraw();
});
},
@ -384,6 +389,7 @@ function PostView() {
const title = meta.mMsgName || p.mMsgName || p.title || 'Post';
const notes = util.plainText(p.mNotes || p.mBody || p.notes || p.body || '');
const hasLongNotes = notes.length > 280;
const author = meta.mAuthorId ? meta.mAuthorId.substring(0, 10) : 'Unknown';
const publishTs = meta.mPublishTs || p.mPublishTs || null;
const dateStr = publishTs
@ -425,26 +431,46 @@ function PostView() {
m('b', author),
dateStr ? m('span', `${dateStr}`) : null,
]),
notes ? m('p', { style: { whiteSpace: 'pre-wrap', margin: '1rem 0' } }, notes) : null,
notes ? m('.post-description.board-post-description', [
m('.post-description__text', { class: notesExpanded ? '' : 'post-description__text--collapsed', style: { whiteSpace: 'pre-wrap', maxHeight: notesExpanded ? 'none' : '4.5em', overflow: 'hidden', lineHeight: '1.5' } }, notes),
hasLongNotes ? m('button.post-description__toggle[type=button]', { onclick: () => { notesExpanded = !notesExpanded; } }, notesExpanded ? 'Show less' : '…more') : null,
]) : null,
m('hr'),
m('.board-comments', [
m('.board-comments__heading', [m('h3', `${comments.length} Comment${comments.length === 1 ? '' : 's'}`), m('span', [m('i.fas.fa-sort-amount-down'), ' Oldest first'])]),
m('.board-comments__heading', [
m('h3', `${comments.length} Comment${comments.length === 1 ? '' : 's'}`),
m('span', [m('i.fas.fa-sort-amount-down'), ' Oldest first']),
m('.board-comments__voter', [
m('label[for=board-comment-voter]', 'Voter identity'),
m('select#board-comment-voter', {
value: voteIdentity || '',
disabled: identities.length === 0,
onchange: (e) => { voteIdentity = e.target.value; },
}, identities.length
? identities.map((id) => m('option', { value: id }, nameOf(id)))
: m('option', { value: '' }, 'Loading identities…')),
]),
]),
m('.board-comment-composer', [
m('.board-comment-avatar', initials(nameOf(authorId))),
m('.board-comment-composer__body', [
replyTo ? m('.board-comment-composer__replying', ['Replying to ', m('b', nameOf(metaOf(replyTo).mAuthorId)), m('button[type=button][aria-label=Cancel reply]', { onclick: () => { replyTo = null; composerText = ''; } }, m('i.fas.fa-times'))]) : null,
identities.length > 1 ? m('select.board-comment-composer__identity', { value: authorId, onchange: (e) => { authorId = e.target.value; } }, identities.map((id) => m('option', { value: id }, nameOf(id)))) : null,
identities.length ? m('select.board-comment-composer__identity', { value: authorId, onchange: (e) => { authorId = e.target.value; } }, identities.map((id) => m('option', { value: id }, nameOf(id)))) : null,
m('textarea.board-comment-composer__input[rows=1][placeholder=Add a comment…]', { value: composerText, disabled: !authorId || submitting, oninput: (e) => { composerText = e.target.value; }, onkeydown: (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') submitComment(forumId, msgId); } }),
!authorId ? m('p.board-comment-composer__hint', 'Create or select an identity to post a comment.') : null,
submitError ? m('p.board-comment-composer__error', submitError) : null,
m('.board-comment-composer__actions', [
m('.board-comment-composer__emoji', { style: { position: 'relative', marginRight: 'auto' } }, [
m('button[type=button][title=Insert emoji][aria-label=Insert emoji]', { style: { width: '32px', height: '32px', padding: '0', borderRadius: '50%', border: '0', boxShadow: 'none', background: showEmojiPicker ? '#e0f2fe' : 'transparent', color: '#475569', fontSize: '1.15rem' }, onclick: () => { showEmojiPicker = !showEmojiPicker; } }, m('i.fas.fa-smile')),
showEmojiPicker ? m('.board-comment-emoji-popover', { style: { position: 'absolute', zIndex: '20', top: '38px', left: '0', width: '250px', maxHeight: '180px', overflowY: 'auto', padding: '.5rem', display: 'grid', gridTemplateColumns: 'repeat(8, 1fr)', gap: '.2rem', background: '#fff', border: '1px solid #cbd5e1', borderRadius: '8px', boxShadow: '0 8px 20px rgba(0,0,0,.16)' } }, chatEmoji.EMOJI_DATA.Smileys.slice(0, 48).map((emoji) => m('button[type=button]', { style: { width: '28px', height: '28px', padding: '0', border: '0', boxShadow: 'none', background: 'transparent', fontSize: '1.1rem' }, onclick: () => { composerText += emoji; showEmojiPicker = false; } }, emoji))) : null,
]),
composerText || replyTo ? m('button.board-comment-composer__cancel[type=button]', { onclick: () => { composerText = ''; replyTo = null; submitError = ''; } }, 'Cancel') : null,
m('button.board-comment-composer__submit[type=button]', { disabled: !composerText.trim() || !authorId || submitting, onclick: () => submitComment(forumId, msgId) }, submitting ? 'Posting…' : 'Comment')
])
])
]),
loadingComments ? m('.board-comments__status', [m('i.fas.fa-spinner.fa-spin'), ' Loading comments…'])
: comments.length === 0 ? m('.board-comments__empty', [m('i.far.fa-comment'), m('p', 'No comments yet. Start the conversation.')])
: comments.length === 0 ? m('.board-comments__empty', [m('i.fas.fa-comment'), m('p', 'No comments yet. Start the conversation.')])
: m('.board-comments__list', treeOfComments().map((node) => renderComment(node, 0))),
]),
]),
@ -468,7 +494,14 @@ function PostView() {
]),
m('p.board-comment__text', textOf(comment)),
m('.board-comment__actions', [
m('span.board-comment__like', [m('i.far.fa-thumbs-up'), ' Like']),
m('button[type=button]', {
disabled: !voteIdentity,
onclick: () => util.voteForPost(forumId, key, util.GXS_VOTE_UP, voteIdentity),
}, [m('i.fas.fa-thumbs-up'), ` ${comment.mUpVotes || 0}`]),
m('button[type=button]', {
disabled: !voteIdentity,
onclick: () => util.voteForPost(forumId, key, util.GXS_VOTE_DOWN, voteIdentity),
}, m('i.fas.fa-thumbs-down')),
m('button[type=button]', { onclick: () => { replyTo = comment; composerText = ''; submitError = ''; } }, 'Reply')
]),
repliesCount ? m('button.board-comment__replies-toggle[type=button]', {

View File

@ -245,15 +245,18 @@ function popupmessage(message) {
);
}
async function voteForPost(postGrpId, postMsgId, voteType) {
async function voteForPost(postGrpId, postMsgId, voteType, voterId = null) {
try {
const resId = await rs.rsJsonApiRequest('/rsIdentity/getOwnIds', {});
const ownIds = (resId && resId.body && resId.body.ids) ? resId.body.ids : [];
if (ownIds.length === 0) {
alert('No identity found to vote.');
return false;
let authorId = voterId;
if (!authorId) {
const resId = await rs.rsJsonApiRequest('/rsIdentity/getOwnIds', {});
const ownIds = (resId && resId.body && resId.body.ids) ? resId.body.ids : [];
if (ownIds.length === 0) {
alert('No identity found to vote.');
return false;
}
authorId = ownIds[0];
}
const authorId = ownIds[0];
const res = await rs.rsJsonApiRequest('/rsPosted/voteForPost', {
postGrpId: postGrpId,

View File

@ -7,6 +7,7 @@ const peopleUtil = require('people/people_util');
const sha1 = require('channels/sha1');
const fileUtil = require('files/files_util');
const fileDown = require('files/files_downloads');
const chatEmoji = require('chat/chat_emoji');
const filesUploadHashes = {
// figure out a better way later.
@ -14,6 +15,29 @@ const filesUploadHashes = {
Thumbnail: [],
};
function channelThumbnailSrc(post) {
const thumbnail = post && (post.mThumbnail || post.thumbnail || post.mImage);
const base64 = thumbnail && thumbnail.mData && thumbnail.mData.base64
? thumbnail.mData.base64
: typeof thumbnail === 'string'
? thumbnail
: thumbnail && thumbnail.base64;
if (!base64 || !String(base64).trim()) return '';
return String(base64).startsWith('data:') ? base64 : `data:image/png;base64,${base64}`;
}
const ChannelFallbackThumbnail = () => ({
view: (vnode) => m('.channel-post__placeholder', { style: {
display: vnode.attrs.hidden ? 'none' : 'flex', flex: '1 1 auto', minHeight: '0',
flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '.35rem',
color: '#64748b', background: 'linear-gradient(135deg, #f8fafc, #dbe5f1)',
} }, [
m('i.fas.fa-image[aria-hidden=true]', { style: { fontSize: '1.35rem', color: '#64748b' } }),
m('span', { style: { fontSize: '2rem', fontWeight: '700', color: '#2563eb' } }, (vnode.attrs.title || 'Post').trim().slice(0, 1).toUpperCase()),
m('small', { style: { fontSize: '.72rem', fontWeight: '600' } }, 'No image'),
]),
});
async function parsefile(file, type) {
const fileSize = file.size;
const chunkSize = 1024 * 1024; // bytes
@ -428,7 +452,14 @@ const ChannelView = () => {
m(
'.posts-container-card',
{
style: 'display: ' + (plist[key].isSearched ? 'flex' : 'none'), // for search
style: {
display: plist[key].isSearched ? 'flex' : 'none', // for search
height: '240px',
minHeight: '0',
overflow: 'hidden',
flexDirection: 'column',
alignSelf: 'start',
},
onclick: () => {
m.route.set('/channels/:tab/:mGroupId/:mMsgId', {
tab: m.route.param().tab,
@ -438,13 +469,19 @@ const ChannelView = () => {
},
},
[
m('img', {
src:
plist[key].post.mThumbnail.mData.base64 === ''
? 'data/streaming.png'
: 'data:image/png;base64,' + plist[key].post.mThumbnail.mData.base64,
alt: 'No Thumbnail',
}),
channelThumbnailSrc(plist[key].post)
? [
m('img', {
src: channelThumbnailSrc(plist[key].post),
alt: plist[key].post.mMeta.mMsgName || 'Post thumbnail',
onerror: (e) => {
e.target.style.display = 'none';
if (e.target.nextSibling) e.target.nextSibling.style.display = 'flex';
},
}),
m(ChannelFallbackThumbnail, { title: plist[key].post.mMeta.mMsgName, hidden: true }),
]
: m(ChannelFallbackThumbnail, { title: plist[key].post.mMeta.mMsgName }),
m('p', plist[key].post.mMeta.mMsgName),
]
),
@ -665,20 +702,156 @@ function displaycomment() {
};
}
/* Modern threaded comment experience for channel posts. */
const ChannelComments = () => {
let replyTo = null;
let text = '';
let identity = null;
let submitting = false;
let error = '';
let showEmojiPicker = false;
const expandedReplies = {};
const metaOf = (comment) => (comment && comment.mMeta) || {};
const idOf = (comment) => metaOf(comment).mMsgId || comment.msgId;
const nameOf = (id) => rs.userList.username(id) || rs.userList.userMap[id] || `${String(id || 'Unknown').slice(0, 10)}`;
const initials = (name) => String(name || '?').split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]).join('').toUpperCase();
const dateOf = (value) => {
const seconds = value && typeof value === 'object' ? value.xint64 : value;
return Number(seconds) ? new Date(Number(seconds) * 1000).toLocaleString() : '';
};
function tree(threadId) {
const nodes = {};
const roots = [];
Object.keys(Data.Comments[threadId] || {}).forEach((key) => {
const entry = Data.Comments[threadId][key];
const comment = entry.comment || entry;
if (idOf(comment)) nodes[idOf(comment)] = { comment, children: [] };
});
Object.keys(nodes).forEach((key) => {
const node = nodes[key];
const parent = metaOf(node.comment).mParentId;
if (parent && parent !== threadId && nodes[parent]) nodes[parent].children.push(node);
else roots.push(node);
});
const chronological = (a, b) => Number(metaOf(a.comment).mPublishTs && (metaOf(a.comment).mPublishTs.xint64 || metaOf(a.comment).mPublishTs)) - Number(metaOf(b.comment).mPublishTs && (metaOf(b.comment).mPublishTs.xint64 || metaOf(b.comment).mPublishTs));
roots.sort(chronological);
Object.keys(nodes).forEach((key) => nodes[key].children.sort(chronological));
return roots;
}
async function submit(vnode) {
const comment = text.trim();
if (!comment || !identity || submitting) return;
submitting = true;
error = '';
try {
const res = await rs.rsJsonApiRequest('/rsgxschannels/createCommentV2', {
channelId: vnode.attrs.channelId,
threadId: vnode.attrs.threadId,
comment,
authorId: identity,
parentId: replyTo ? idOf(replyTo) : vnode.attrs.threadId,
});
if (!res || !res.body || res.body.retval === false) {
error = (res && res.body && res.body.errorMessage) || 'Your comment could not be posted.';
return;
}
text = '';
replyTo = null;
await util.updatedisplaychannels(vnode.attrs.channelId);
} catch (submitError) {
console.warn('Channel comment submission failed', submitError);
error = 'Your comment could not be posted. Please try again.';
} finally {
submitting = false;
m.redraw();
}
}
function renderComment(node, vnode) {
const comment = node.comment;
const meta = metaOf(comment);
const id = idOf(comment);
const name = nameOf(meta.mAuthorId);
const votes = (Data.Votes[meta.mThreadId] && Data.Votes[meta.mThreadId][id]) || { upvotes: 0, downvotes: 0 };
const repliesExpanded = expandedReplies[id] === true;
return m('.board-comment', { key: id }, [
m('.board-comment-avatar', initials(name)),
m('.board-comment__content', [
m('.board-comment__header', [
m('.board-comment__meta', [m('b', name), dateOf(meta.mPublishTs) ? m('span', dateOf(meta.mPublishTs)) : null]),
m('button.board-comment__menu[type=button][aria-label=Comment options]', m('i.fas.fa-ellipsis-v')),
]),
m('p.board-comment__text', comment.mComment || comment.comment || ''),
m('.board-comment__actions', [
m('button[type=button]', { disabled: !vnode.attrs.voteIdentity, onclick: () => addvote(util.GXS_VOTE_UP, vnode.attrs.channelId, vnode.attrs.threadId, vnode.attrs.voteIdentity, id) }, [m('i.fas.fa-thumbs-up'), ` ${votes.upvotes || 0}`]),
m('button[type=button]', { disabled: !vnode.attrs.voteIdentity, onclick: () => addvote(util.GXS_VOTE_DOWN, vnode.attrs.channelId, vnode.attrs.threadId, vnode.attrs.voteIdentity, id) }, m('i.fas.fa-thumbs-down')),
m('button[type=button]', { onclick: () => { replyTo = comment; text = ''; error = ''; } }, 'Reply'),
]),
node.children.length ? m('button.board-comment__replies-toggle[type=button]', { 'aria-expanded': repliesExpanded, onclick: () => { expandedReplies[id] = !repliesExpanded; } }, [`${node.children.length} ${node.children.length === 1 ? 'reply' : 'replies'} `, m('i.fas', { class: repliesExpanded ? 'fa-chevron-up' : 'fa-chevron-down' })]) : null,
node.children.length && repliesExpanded ? m('.board-comment__replies', node.children.map((child) => renderComment(child, vnode))) : null,
]),
]);
}
return {
view: (vnode) => {
const identities = (vnode.attrs.identities || []).filter((id) => Number(id) !== 0);
if (!identity && identities.length) identity = identities[0];
const comments = tree(vnode.attrs.threadId);
return m('.board-comments.channel-comments', [
m('.board-comments__heading', [
m('h3', `${Object.keys(Data.Comments[vnode.attrs.threadId] || {}).length} Comment${Object.keys(Data.Comments[vnode.attrs.threadId] || {}).length === 1 ? '' : 's'}`),
m('span', [m('i.fas.fa-sort-amount-down'), ' Oldest first']),
m('.board-comments__voter', [
m('label[for=channel-comment-voter]', 'Voter identity'),
m('select#channel-comment-voter', {
value: vnode.attrs.voteIdentity || '',
disabled: identities.length === 0,
onchange: (e) => vnode.attrs.onVoteIdentity(e.target.value),
}, identities.length
? identities.map((id) => m('option', { value: id }, nameOf(id)))
: m('option', { value: '' }, vnode.attrs.identitiesLoading ? 'Loading identities…' : 'No identity available')),
]),
]),
m('.board-comment-composer', [
m('.board-comment-avatar', initials(nameOf(identity))),
m('.board-comment-composer__body', [
replyTo ? m('.board-comment-composer__replying', ['Replying to ', m('b', nameOf(metaOf(replyTo).mAuthorId)), m('button[type=button][aria-label=Cancel reply]', { onclick: () => { replyTo = null; text = ''; } }, m('i.fas.fa-times'))]) : null,
identities.length ? m('select.board-comment-composer__identity', { value: identity, onchange: (e) => { identity = e.target.value; } }, identities.map((id) => m('option', { value: id }, nameOf(id)))) : null,
m('textarea.board-comment-composer__input[rows=1][placeholder=Add a comment…]', { value: text, disabled: !identity || submitting, oninput: (e) => { text = e.target.value; }, onkeydown: (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') submit(vnode); } }),
!identity ? m('p.board-comment-composer__hint', vnode.attrs.identitiesLoading ? 'Loading identities…' : 'Create or select an identity to post a comment.') : null,
error ? m('p.board-comment-composer__error', error) : null,
m('.board-comment-composer__actions', [
m('.board-comment-composer__emoji', { style: { position: 'relative', marginRight: 'auto' } }, [
m('button[type=button][title=Insert emoji][aria-label=Insert emoji]', { style: { width: '32px', height: '32px', padding: '0', borderRadius: '50%', border: '0', boxShadow: 'none', background: showEmojiPicker ? '#e0f2fe' : 'transparent', color: '#475569', fontSize: '1.15rem' }, onclick: () => { showEmojiPicker = !showEmojiPicker; } }, m('i.fas.fa-smile')),
showEmojiPicker ? m('.board-comment-emoji-popover', { style: { position: 'absolute', zIndex: '20', top: '38px', left: '0', width: '250px', maxHeight: '180px', overflowY: 'auto', padding: '.5rem', display: 'grid', gridTemplateColumns: 'repeat(8, 1fr)', gap: '.2rem', background: '#fff', border: '1px solid #cbd5e1', borderRadius: '8px', boxShadow: '0 8px 20px rgba(0,0,0,.16)' } }, chatEmoji.EMOJI_DATA.Smileys.slice(0, 48).map((emoji) => m('button[type=button]', { style: { width: '28px', height: '28px', padding: '0', border: '0', boxShadow: 'none', background: 'transparent', fontSize: '1.1rem' }, onclick: () => { text += emoji; showEmojiPicker = false; } }, emoji))) : null,
]),
text || replyTo ? m('button.board-comment-composer__cancel[type=button]', { onclick: () => { text = ''; replyTo = null; error = ''; } }, 'Cancel') : null,
m('button.board-comment-composer__submit[type=button]', { disabled: !text.trim() || !identity || submitting, onclick: () => submit(vnode) }, submitting ? 'Posting…' : 'Comment'),
]),
]),
]),
comments.length ? m('.board-comments__list', comments.map((node) => renderComment(node, vnode))) : m('.board-comments__empty', [m('i.fas.fa-comment'), m('p', 'No comments yet. Start the conversation.')]),
]);
},
};
};
const PostView = () => {
let post = {};
let topComments = {};
const filesInfo = {};
let voteIdentity;
let ownId;
let identitiesLoading = true;
let messageExpanded = false;
return {
oninit: async (v) => {
if (Data.Posts[v.attrs.channelId] && Data.Posts[v.attrs.channelId][v.attrs.msgId]) {
post = Data.Posts[v.attrs.channelId][v.attrs.msgId].post;
}
if (Data.TopComments[v.attrs.msgId]) {
topComments = Data.TopComments[v.attrs.msgId]; // get all the top level parent comments
}
if (post) {
post.mFiles.map(async (file) => {
const res = await rs.rsJsonApiRequest('/rsfiles/alreadyHaveFile', {
@ -696,10 +869,16 @@ const PostView = () => {
}
}
voteIdentity = ownId[0];
identitiesLoading = false;
});
fileDown.Downloads.loadStatus(); // for retrieving downloading files.
},
view: (v) => [
view: (v) => {
const message = post.mMsg || '';
const messageText = String(message).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
const hasEmbeddedImage = /<img\b|data:image\//i.test(String(message));
const hasLongMessage = messageText.length > 280 || hasEmbeddedImage;
return [
m(
'a[title=Back]',
{
@ -713,7 +892,19 @@ const PostView = () => {
),
m('.widget__heading', m('h3', post.mMeta.mMsgName)),
m('.widget__body', [
m('p', { style: { whiteSpace: 'normal' } }, m.trust(post.mMsg)),
message ? m('.post-description', [
m('.post-description__text', {
style: {
maxHeight: messageExpanded ? 'none' : '4.5em',
overflow: 'hidden',
lineHeight: '1.5',
},
}, m.trust(message)),
hasLongMessage ? m('button.post-description__toggle[type=button]', {
style: { marginTop: '.35rem', padding: '0', border: '0', boxShadow: 'none', background: 'transparent', color: '#0f172a', fontSize: '.85rem', fontWeight: '700' },
onclick: () => { messageExpanded = !messageExpanded; },
}, messageExpanded ? 'Show less' : '…more') : null,
]) : null,
m('.file-section', [
m('h3', 'Files(' + post.mAttachmentCount + ')'),
m(
@ -722,132 +913,77 @@ const PostView = () => {
'tbody',
post.mFiles.map((file) =>
m('tr', [
m('td', file.mName),
m('td', rs.formatBytes(file.mSize.xint64)),
m(
'button',
{
style: { fontSize: '0.9em' },
onclick: async () =>
widget.popupMessage([
m('p', 'Start Download?'),
m(
'button',
{
onclick: async () => {
if (filesInfo[file.mHash] && !filesInfo[file.mHash].retval) {
const res = await rs.rsJsonApiRequest('/rsFiles/FileRequest', {
fileName: file.mName,
hash: file.mHash,
flags: util.RS_FILE_REQ_ANONYMOUS_ROUTING,
size: {
xstr64: file.mSize.xstr64,
},
});
res.body.retval === false
? widget.popupMessage([
m('h3', 'Error'),
m('hr'),
m('p', res.body.errorMessage),
])
: widget.popupMessage([
m('h3', 'Success'),
m('hr'),
m('p', 'Download Started'),
]);
m.redraw();
}
m('td.channel-file__name[data-label=File name]', file.mName),
m('td.channel-file__size[data-label=Size]', rs.formatBytes(file.mSize.xint64)),
m('td.channel-file__action[data-label=Download]', [
m(
'button',
{
style: { fontSize: '0.9em' },
onclick: async () =>
widget.popupMessage([
m('p', 'Start Download?'),
m(
'button',
{
onclick: async () => {
if (filesInfo[file.mHash] && !filesInfo[file.mHash].retval) {
const res = await rs.rsJsonApiRequest('/rsFiles/FileRequest', {
fileName: file.mName,
hash: file.mHash,
flags: util.RS_FILE_REQ_ANONYMOUS_ROUTING,
size: {
xstr64: file.mSize.xstr64,
},
});
res.body.retval === false
? widget.popupMessage([
m('h3', 'Error'),
m('hr'),
m('p', res.body.errorMessage),
])
: widget.popupMessage([
m('h3', 'Success'),
m('hr'),
m('p', 'Download Started'),
]);
m.redraw();
}
},
},
},
'Start Download'
),
]),
},
filesInfo[file.mHash]
? filesInfo[file.mHash].retval
? 'Open File'
: ['Download', m('i.fas.fa-download')]
: 'Please Wait...'
),
fileDown.list[file.mHash] && // using the file from files_util to display download.
m(fileUtil.File, {
'Start Download'
),
]),
},
filesInfo[file.mHash]
? filesInfo[file.mHash].retval
? 'Open File'
: ['Download ', m('i.fas.fa-download')]
: 'Please Wait...'
),
fileDown.list[file.mHash] && m(fileUtil.File, {
info: fileDown.list[file.mHash],
direction: 'down',
transferred: fileDown.list[file.mHash].transfered.xint64,
parts: [],
}),
]),
])
)
)
),
]),
m('.comments-section', [
m('h3', 'Comments'),
m('.comments-section__menu', [
m(
'button',
{
onclick: () => {
widget.popupMessage(
m(AddComment, {
parent_comment: '',
channelId: v.attrs.channelId,
authorId: ownId,
threadId: v.attrs.msgId,
parentId: v.attrs.msgId,
})
);
},
},
'Add Comment'
),
m('.comments-section__menu-id', [
m('label[for=idtags', 'Voter ID: '),
m(
'select[id=idtags]',
{
value: voteIdentity,
onchange: (e) => {
voteIdentity = ownId[e.target.selectedIndex];
},
},
[
ownId &&
ownId.map((o) =>
m(
'option',
{ value: o },
`${rs.userList.userMap[o].toLocaleString()} (${o.slice(0, 8)}...)`
)
),
]
),
]),
]),
]),
m(
util.CommentsTable,
m(
'tbody',
Object.keys(topComments).map((key, index) =>
Data.Comments[topComments[key].mMeta.mThreadId] &&
Data.Comments[topComments[key].mMeta.mThreadId][topComments[key].mMeta.mMsgId]
? m(displaycomment, {
// calls the recursive function for all the parents.
identity: ownId,
voteIdentity,
commentStruct:
Data.Comments[topComments[key].mMeta.mThreadId][
topComments[key].mMeta.mMsgId
],
replyDepth: 0,
})
: ''
)
)
),
m(ChannelComments, {
channelId: v.attrs.channelId,
threadId: v.attrs.msgId,
identities: ownId,
voteIdentity,
identitiesLoading,
onVoteIdentity: (id) => { voteIdentity = id; },
}),
]),
],
];
},
};
};

View File

@ -173,8 +173,8 @@ const FilesTable = () => {
return {
oninit: (v) => {},
view: (v) =>
m('table.files', [
m('tr', [m('th', 'File Name'), m('th', 'Size'), m('th', m('i.fas.fa-download'))]),
m('table.files.channel-files', [
m('thead', m('tr', [m('th', 'File Name'), m('th', 'Size'), m('th', m('i.fas.fa-download'))])),
v.children,
]),
};

View File

@ -137,15 +137,21 @@ function sortIds(list) {
}
async function ownIds(consumer = () => { }, onlySigned = false) {
await rs.rsJsonApiRequest('/rsIdentity/getOwnSignedIds', {}, (owns) => {
try {
const signedResponse = await rs.rsJsonApiRequest('/rsIdentity/getOwnSignedIds', {});
const signedIds = (signedResponse && signedResponse.body && signedResponse.body.ids) || [];
if (onlySigned) {
consumer(sortIds(owns.ids));
} else {
rs.rsJsonApiRequest('/rsIdentity/getOwnPseudonimousIds', {}, (pseudo) => {
if (pseudo.ids) consumer(sortIds(pseudo.ids.concat(owns.ids)));
});
consumer(sortIds(signedIds));
return;
}
});
const pseudonymousResponse = await rs.rsJsonApiRequest('/rsIdentity/getOwnPseudonimousIds', {});
const pseudonymousIds = (pseudonymousResponse && pseudonymousResponse.body && pseudonymousResponse.body.ids) || [];
consumer(sortIds(pseudonymousIds.concat(signedIds)));
} catch (error) {
console.warn('Unable to load own identities', error);
consumer([]);
}
}
const SearchBar = () => {
let searchString = '';

View File

@ -240,6 +240,8 @@
.board-comments__heading h3 { margin: 0; font-size: 1.15rem; }
.board-comments__heading span { color: #64748b; font-size: .8rem; font-weight: 600; }
.board-comments__heading i { margin-right: .35rem; }
.board-comments__voter { display: inline-flex; align-items: center; gap: .4rem; margin-left: auto; color: #64748b; font-size: .75rem; font-weight: 600; white-space: nowrap; }
.board-comments__voter select { max-width: 180px; padding: .25rem .4rem; font-size: .78rem; }
.board-comment-composer,
.board-comment {
@ -355,7 +357,9 @@
.board-comments__empty i { font-size: 1.5rem; }
@media (max-width: 560px) {
.board-comments__heading { justify-content: space-between; }
.board-comments__heading { flex-wrap: wrap; justify-content: space-between; gap: .5rem; }
.board-comments__voter { width: 100%; margin-left: 0; }
.board-comments__voter select { flex: 1; max-width: none; }
.board-comment-composer, .board-comment { gap: .6rem; }
.board-comment-avatar { flex-basis: 32px; width: 32px; height: 32px; font-size: .68rem; }
.board-comment__replies { padding-left: .6rem; }
@ -604,6 +608,17 @@ button.board-card__vote-btn,
background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e1 100%) !important;
}
.board-card__placeholder-content {
display: flex;
flex-direction: column;
align-items: center;
gap: .3rem;
color: #64748b;
font-size: .72rem;
font-weight: 600;
}
.board-card__placeholder-content i { font-size: 1.35rem; }
.board-card--compact .board-card__placeholder-img {
width: 24px !important;
height: 24px !important;

View File

@ -78,6 +78,116 @@ table {
}
}
}
.posts-container-card .channel-post__placeholder {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: .35rem;
color: #64748b;
background: linear-gradient(135deg, #f8fafc, #dbe5f1);
}
.channel-post__placeholder i { font-size: 1.35rem; color: #64748b; }
.channel-post__placeholder span { font-size: 2rem; font-weight: 700; color: #2563eb; }
.channel-post__placeholder small { font-size: .72rem; font-weight: 600; }
/* Compact YouTube-style post description with an opt-in expansion control. */
.post-description {
margin: 1rem 0;
padding: .85rem 1rem;
border-radius: 10px;
background: #f1f5f9;
color: #1e293b;
}
.post-description__text { overflow-wrap: anywhere; line-height: 1.5; }
.post-description__text > :first-child { margin-top: 0; }
.post-description__text > :last-child { margin-bottom: 0; }
.post-description__text--collapsed {
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}
.post-description__toggle {
margin-top: .35rem;
padding: 0 !important;
border: 0 !important;
box-shadow: none !important;
background: transparent !important;
color: #0f172a !important;
font-size: .85rem !important;
font-weight: 700 !important;
}
.post-description__toggle:hover { color: #2563eb !important; text-decoration: underline; }
/* Keep image and generated-thumbnail cards the same size instead of stretching
* a sparse grid row to the full height of the Posts panel. */
.posts-container { align-content: start; grid-auto-rows: 240px; }
.posts-container-card { height: 240px; min-height: 0; overflow: hidden; }
.posts-container-card > img,
.posts-container-card > .channel-post__placeholder { min-height: 0; }
@media (max-width: 700px) {
.posts-container { grid-auto-rows: 210px; gap: 1rem; }
.posts-container-card { height: 210px; }
}
/* Channel attachment table -> compact cards on phones. */
@media (max-width: 700px) {
.file-section { margin-top: 1.25rem; }
table.channel-files,
table.channel-files tbody,
table.channel-files tr,
table.channel-files td {
display: block;
width: 100% !important;
box-sizing: border-box;
}
table.channel-files { padding: 0; table-layout: auto; }
table.channel-files thead { display: none; }
table.channel-files tr {
margin: 0 0 .7rem;
padding: .7rem;
border: 1px solid #dbe3ef;
border-radius: 8px;
background: #fff;
}
table.channel-files td {
min-width: 0;
padding: .25rem 0;
border: 0;
text-align: left;
}
table.channel-files td::before {
display: block;
margin-bottom: .1rem;
color: #64748b;
content: attr(data-label);
font-size: .72rem;
font-weight: 700;
text-transform: uppercase;
}
table.channel-files .channel-file__name {
overflow-wrap: anywhere;
color: #0f172a;
font-weight: 600;
line-height: 1.35;
}
table.channel-files .channel-file__size { color: #475569; }
table.channel-files .channel-file__action { padding-top: .5rem; }
table.channel-files .channel-file__action > button { min-width: 116px; }
table.channel-files .channel-file__action .file-view { margin-top: .6rem; }
}
/* #options{
width: 100px;
text-align: center;

File diff suppressed because it is too large Load Diff