mirror of
https://github.com/RetroShare/RSNewWebUI.git
synced 2026-09-14 11:05:47 +05:00
Improved Channels for Phone & Desktop
Fixed channels thumbnail layout issue Added same comments design from boards to channels
This commit is contained in:
parent
04d42dc38f
commit
709c0022f0
@ -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
|
||||
|
||||
@ -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]', {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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; },
|
||||
}),
|
||||
]),
|
||||
],
|
||||
];
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -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,
|
||||
]),
|
||||
};
|
||||
|
||||
@ -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 = '';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
6098
webui-src/styles.css
6098
webui-src/styles.css
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user