diff --git a/data/streaming.png b/data/streaming.png new file mode 100644 index 0000000..02eafda Binary files /dev/null and b/data/streaming.png differ diff --git a/webui-src/app/channels/channel.css b/webui-src/app/channels/channel.css index 76a1b28..17f3ada 100644 --- a/webui-src/app/channels/channel.css +++ b/webui-src/app/channels/channel.css @@ -97,7 +97,15 @@ table.comments td:nth-child(2) { table.comments tr { height: 40px; } - +table.files th:nth-child(1) { + width: 40%; +} +table.files th:nth-child(3) { + width: 50%; +} +table.files td { + word-wrap: break-word; +} /* #options{ width: 100px; text-align: center; @@ -105,3 +113,10 @@ table.comments tr { margin-left: 20px; height: 40px; } */ +#mtags { + width: 160px; + text-align: center; + font-size: medium; + margin-left: 10px; + height: 40px; +} diff --git a/webui-src/app/channels/channel_view.js b/webui-src/app/channels/channel_view.js index 2be2b5a..894456e 100644 --- a/webui-src/app/channels/channel_view.js +++ b/webui-src/app/channels/channel_view.js @@ -1,6 +1,7 @@ const m = require('mithril'); const rs = require('rswebui'); const util = require('channels/channels_util'); +const widget = require('widgets'); const Data = util.Data; const peopleUtil = require('people/people_util'); const sha1 = require('channels/sha1'); @@ -72,15 +73,164 @@ async function parsefile(file, type) { await chunkreaderblock(offset, chunkSize, file); return ansList; } +const messageGroups = ['Public', 'Restricted Circle', 'Restricted Node Group']; +const messageGroupsCode = [util.PUBLIC, util.EXTERNAL, util.NODES_GROUP]; //rsgxscirles.h:50 + +function createchannel() { + let title; + let body; + let identity; + let thumbnail; + let selectedGroup = messageGroups[0]; + let selectedGroupCode = messageGroupsCode[0]; + let selectedCircle; + let circles; + return { + oninit: async (vnode) => { + if (vnode.attrs.authorId) { + identity = vnode.attrs.authorId[0]; + } + + const res = await rs.rsJsonApiRequest('/rsgxscircles/getCirclesSummaries'); + if (res.body.retval) { + circles = res.body.circles; + selectedCircle = circles[0].mGroupName; + } + }, + view: (vnode) => + m('.widget', [ + m('h3', 'Create Channel'), + m('hr'), + m('input[type=text][placeholder=Title]', { + style: { float: 'left' }, + oninput: (e) => (title = e.target.value), + }), + m('div', { style: { float: 'right', marginTop: '10px', marginBottom: '10px' } }, [ + m('label[for=thumbnail]', 'Thumbnail: '), + m('input[type=file][name=files][id=thumbnail][accept=image/*]', { + onchange: async (e) => { + let reader = new FileReader(); + reader.onloadend = function () { + thumbnail = reader.result.substring(reader.result.indexOf(',') + 1); + }; + reader.readAsDataURL(e.target.files[0]); + }, + }), + ]), + + m('div', { style: { float: 'right', marginTop: '10px', marginBottom: '10px' } }, [ + m('label[for=idtags]', 'Select identity: '), + m( + 'select[id=idtags]', + { + value: identity, + onchange: (e) => { + identity = vnode.attrs.authorId[e.target.selectedIndex]; + }, + }, + [ + vnode.attrs.authorId && + vnode.attrs.authorId.map((o) => + m( + 'option', + { value: o }, + rs.userList.userMap[o] + ? rs.userList.userMap[o].toLocaleString() + ' (' + o.slice(0, 8) + '...)' + : 'No Signature' + ) + ), + ] + ), + ]), + m('div', { style: { float: 'left', marginTop: '10px', marginBottom: '10px' } }, [ + m('label[for=mtags]', 'Message Distribution: '), + m( + 'select[id=mtags]', + { + value: selectedGroup, + onchange: (e) => { + selectedGroup = messageGroups[e.target.selectedIndex]; + selectedGroupCode = messageGroupsCode[e.target.selectedIndex]; + widget.popupMessage(m(createchannel, { authorId: vnode.attrs.authorId })); + }, + }, + [messageGroups.map((group) => m('option', { value: group }, group))] + ), + ]), + circles && + m( + 'div', + { + style: { + float: 'left', + marginTop: '10px', + marginBottom: '10px', + display: selectedGroupCode === util.EXTERNAL ? 'block' : 'none', + }, + }, + [ + m('label[for=circlestag]', 'Circles: '), + m( + 'select[id=circlestag]', + { + value: selectedCircle, + onchange: (e) => { + selectedCircle = circles[e.target.selectedIndex]; + console.log(selectedCircle); + // selectedGroupCode = messageGroupsCode[e.target.selectedIndex]; + }, + }, + [ + circles.map((circle) => + m('option', { value: circle.mGroupName }, circle.mGroupName) + ), + ] + ), + ] + ), + m('textarea[rows=5][placeholder=Description]', { + style: { width: '100%', display: 'block' }, + oninput: (e) => (body = e.target.value), + value: body, + }), + m( + 'button', + { + onclick: async () => { + const res = await rs.rsJsonApiRequest('/rsgxschannels/createChannelV2', { + name: title, + description: body, + thumbnail: { mData: { base64: thumbnail } }, + ...(Number(identity) !== 0 && { authorId: identity }), //checks if some identity has to be assigned + circleType: selectedGroupCode, + ...(selectedGroupCode === util.EXTERNAL && + selectedCircle && { circleId: selectedCircle.mGroupId }), //checks if the selectedGroup code is EXTERNAL + }); + if (res.body.retval) { + util.updatedisplaychannels(res.body.channelId); + m.redraw(); + } + 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', 'Channel created successfully'), + ]); + }, + }, + 'Create' + ), + ]), + }; +} const AddPost = () => { let content = ''; let ptitle = ''; let pthumbnail = []; let pfiles = []; - let uploadFiles = false; - let uploadThumbnail = false; - + let uploadFiles = true; return { view: (vnode) => m('.widget', [ @@ -89,23 +239,18 @@ const AddPost = () => { m('label[for=thumbnail]', 'Thumbnail: '), m('input[type=file][name=files][id=thumbnail][accept=image/*]', { onchange: async (e) => { - filesUploadHashes.Thumbnail = []; - pthumbnail = []; - const ansList = await parsefile(e.target.files[0], ''); - - if (filesUploadHashes.Thumbnail.length === e.target.files.length) { - pthumbnail.push({ - name: e.target.files[0].name, - size: e.target.files[0].size, - hash: filesUploadHashes.Thumbnail[0], - }); - uploadThumbnail = true; - } + let reader = new FileReader(); + reader.onloadend = function () { + pthumbnail = reader.result.substring(reader.result.indexOf(',') + 1); + }; + reader.readAsDataURL(e.target.files[0]); // converts into base64 string }, }), m('label[for=browse]', 'Attachments: '), m('input[type=file][name=files][id=browse][multiple=multiple]', { + // attachments option wrong hash, not working onchange: async (e) => { + uploadFiles = false; filesUploadHashes.PostFiles = []; pfiles = []; for (let i = 0; i < e.target.files.length; i++) { @@ -129,7 +274,7 @@ const AddPost = () => { oninput: (e) => (ptitle = e.target.value), }), m('textarea[rows=5]', { - style: {width: '90%', display: 'block'}, + style: { width: '90%', display: 'block' }, oninput: (e) => (content = e.target.value), value: content, }), @@ -137,22 +282,24 @@ const AddPost = () => { 'button', { onclick: async () => { - if (uploadFiles && uploadThumbnail) { + if (uploadFiles) { console.log(vnode.attrs.chanId, ptitle, content, pfiles, pthumbnail); const res = await rs.rsJsonApiRequest('/rsgxschannels/createPostV2', { channelId: vnode.attrs.chanId, title: ptitle, mBody: content, - files: pfiles, - thumbnail: pthumbnail, + files: pfiles, //does not work for now + thumbnail: { mData: { base64: pthumbnail } }, }); res.body.retval === false - ? util.m([m('h3', 'Error'), m('hr'), m('p', res.body.errorMessage)]) - : util.m([ + ? widget.popupMessage([m('h3', 'Error'), m('hr'), m('p', res.body.errorMessage)]) + : widget.popupMessage([ m('h3', 'Success'), m('hr'), m('p', 'Post added successfully'), ]); + util.updatedisplaychannels(vnode.attrs.chanId); + m.redraw(); } }, }, @@ -167,6 +314,7 @@ const ChannelView = () => { let cimage = ''; let cauthor = ''; let csubscribed = {}; + let mychannel = false; let cposts = 0; let plist = {}; let createDate = {}; @@ -184,6 +332,7 @@ const ChannelView = () => { cauthor = 'Unknown'; } csubscribed = Data.DisplayChannels[v.attrs.id].isSubscribed; + mychannel = Data.DisplayChannels[v.attrs.id].mychannel; cposts = Data.DisplayChannels[v.attrs.id].posts; createDate = Data.DisplayChannels[v.attrs.id].created; lastActivity = Data.DisplayChannels[v.attrs.id].activity; @@ -228,7 +377,10 @@ const ChannelView = () => { csubscribed ? 'Subscribed' : 'Subscribe' ), m('img.channelpic', { - src: 'data:image/png;base64,' + cimage.mData.base64, + src: + cimage.mData.base64 === '' + ? 'data/streaming.png' + : 'data:image/png;base64,' + cimage.mData.base64, }), m('[id=channeldetails]', [ m('p', m('b', 'Posts: '), cposts), @@ -257,10 +409,12 @@ const ChannelView = () => { style: 'display:' + (csubscribed ? 'block' : 'none'), }, m('h3', 'Posts'), - m('button', { onclick: () => util.m(m(AddPost, { chanId: v.attrs.id })) }, [ - 'Add Post', - m('i.fas.fa-edit'), - ]), + mychannel && + m( + 'button', + { onclick: () => widget.popupMessage(m(AddPost, { chanId: v.attrs.id })) }, + ['Add Post', m('i.fas.fa-edit')] + ), m('hr'), m( @@ -270,7 +424,7 @@ const ChannelView = () => { 'div', { class: 'card', - style: 'display: ' + (plist[key].isSearched ? 'block' : 'none'), + style: 'display: ' + (plist[key].isSearched ? 'block' : 'none'), // for search onclick: () => { m.route.set('/channels/:tab/:mGroupId/:mMsgId', { tab: m.route.param().tab, @@ -282,7 +436,10 @@ const ChannelView = () => { [ m('img', { class: 'card-img', - src: 'data:image/png;base64,' + plist[key].post.mThumbnail.mData.base64, + src: + plist[key].post.mThumbnail.mData.base64 === '' + ? 'data/streaming.png' + : 'data:image/png;base64,' + plist[key].post.mThumbnail.mData.base64, alt: '', }), @@ -315,16 +472,42 @@ async function addvote(voteType, vchannelId, vpostId, vauthorId, vcommentId) { const AddComment = () => { let inputComment = ''; + let identity; return { + oninit: (vnode) => { + if (vnode.attrs.authorId) { + identity = vnode.attrs.authorId[0]; + } + }, view: (vnode) => m('.widget', [ m('h3', 'Add Comment'), + m('label[for=tags]', 'Select identity'), + m( + 'select[id=idtags]', + { + value: identity, + onchange: (e) => { + identity = vnode.attrs.authorId[e.target.selectedIndex]; + }, + }, + [ + vnode.attrs.authorId && + vnode.attrs.authorId.map((o) => + m( + 'option', + { value: o }, + rs.userList.userMap[o].toLocaleString() + ' (' + o.slice(0, 8) + '...)' + ) + ), + ] + ), m('hr'), (vnode.attrs.parent_comment !== '') > 0 - ? [m('h5', 'Reply to comment: '), m('p', vnode.attrs.parent_comment)] + ? [m('h5', 'Reply to comment: '), m('p', vnode.attrs.parent_comment)] // if it is add reply option : '', m('textarea[rows=5]', { - style: {width: '90%', display: 'block'}, + style: { width: '90%', display: 'block' }, oninput: (e) => (inputComment = e.target.value), value: inputComment, }), @@ -336,13 +519,13 @@ const AddComment = () => { channelId: vnode.attrs.channelId, threadId: vnode.attrs.threadId, comment: inputComment, - authorId: vnode.attrs.authorId, + authorId: identity, parentId: vnode.attrs.parentId, }); res.body.retval === false - ? util.m([m('h3', 'Error'), m('hr'), m('p', res.body.errorMessage)]) - : util.m([ + ? widget.popupMessage([m('h3', 'Error'), m('hr'), m('p', res.body.errorMessage)]) + : widget.popupMessage([ m('h3', 'Success'), m('hr'), m('p', 'Comment added successfully'), @@ -357,9 +540,10 @@ const AddComment = () => { }; }; function displaycomment() { + // recursive function to display comments return { oninit: (v) => {}, - view: ({ attrs: { commentStruct, identity, replyDepth } }) => { + view: ({ attrs: { commentStruct, identity, replyDepth, voteIdentity } }) => { const comment = commentStruct.comment; let parMap = {}; if (Data.ParentCommentMap[comment.mMeta.mMsgId]) { @@ -367,12 +551,12 @@ function displaycomment() { } return [ m('tr', [ - Object.keys(parMap).length + Object.keys(parMap).length // if it has replies ? m( 'td', m('i.fas.fa-angle-right', { class: 'fa-rotate-' + (commentStruct.showReplies ? '90' : '0'), - style: 'margin-top:12px', + style: 'cursor:pointer', onclick: () => { commentStruct.showReplies = !commentStruct.showReplies; }, @@ -386,7 +570,7 @@ function displaycomment() { style: { position: 'relative', '--replyDepth': replyDepth, - left: 'calc(30px*var(--replyDepth))', + left: 'calc(30px*var(--replyDepth))', // shifts the reply by 30px }, }, [ @@ -397,7 +581,7 @@ function displaycomment() { { style: 'font-size:15px', onclick: () => - util.m( + widget.popupMessage( m(AddComment, { parent_comment: comment.mComment, channelId: comment.mMeta.mGroupId, @@ -409,36 +593,38 @@ function displaycomment() { }, 'Reply' ), - m( - 'button', - { - style: 'font-size:15px', - onclick: () => - addvote( - util.GXS_VOTE_UP, - comment.mMeta.mGroupId, - comment.mMeta.mThreadId, - identity, - comment.mMeta.mMsgId - ), - }, - m('i.fas.fa-thumbs-up') - ), - m( - 'button', - { - style: 'font-size:15px', - onclick: () => - addvote( - util.GXS_VOTE_DOWN, - comment.mMeta.mGroupId, - comment.mMeta.mThreadId, - identity, - comment.mMeta.mMsgId - ), - }, - m('i.fas.fa-thumbs-down') - ), + voteIdentity && + m( + 'button', + { + style: 'font-size:15px', + onclick: () => + addvote( + util.GXS_VOTE_UP, + comment.mMeta.mGroupId, + comment.mMeta.mThreadId, + voteIdentity, + comment.mMeta.mMsgId + ), + }, + m('i.fas.fa-thumbs-up') + ), + voteIdentity && + m( + 'button', + { + style: 'font-size:15px', + onclick: () => + addvote( + util.GXS_VOTE_DOWN, + comment.mMeta.mGroupId, + comment.mMeta.mThreadId, + voteIdentity, + comment.mMeta.mMsgId + ), + }, + m('i.fas.fa-thumbs-down') + ), ]), ] ), @@ -454,13 +640,14 @@ function displaycomment() { m('td', comment.mUpVotes), m('td', comment.mDownVotes), ]), - commentStruct.showReplies && + commentStruct.showReplies && // recursive calls for the replies // parMap.map((value) => Object.keys(parMap).map((key, index) => m(displaycomment, { commentStruct: Data.Comments[parMap[key].mMeta.mThreadId][parMap[key].mMeta.mMsgId], + voteIdentity, identity, - replyDepth: replyDepth + 1, + replyDepth: replyDepth + 1, // for the css }) ), ]; @@ -472,26 +659,35 @@ const PostView = () => { let post = {}; let topComments = {}; const filesInfo = {}; - let ownId = {}; + let voteIdentity; + let ownId; return { - oninit: (v) => { + 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]; + 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', { + // checks if the file is already there with the user hash: file.mHash, }); filesInfo[file.mHash] = res.body; }); } - peopleUtil.ownIds((data) => { - ownId = data[0]; + await peopleUtil.ownIds((data) => { + ownId = data; + for (let i = 0; i < ownId.length; i++) { + if (Number(ownId[i]) === 0) { + ownId.splice(i, 1); // workaround for id '0' + } + } + voteIdentity = ownId[0]; }); + fileDown.Downloads.loadStatus(); // for retrieving downloading files. }, view: (v) => m('.widget', { key: v.attrs.msgId }, [ @@ -521,20 +717,55 @@ const PostView = () => { m( 'button', { - onclick: async () => { - filesInfo[file.mHash] - ? filesInfo[file.mHash].retval - ? '' - : await rs.rsJsonApiRequest('/rsFiles/FileRequest', { - fileName: file.mName, - hash: file.mHash, - flags: util.RS_FILE_REQ_ANONYMOUS_ROUTING, - size: { - xstr64: file.mSize.xstr64, - }, - }) - : ''; - }, + 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 + // ? '' + // : (await rs.rsJsonApiRequest('/rsFiles/FileRequest', { + // fileName: file.mName, + // hash: file.mHash, + // flags: util.RS_FILE_REQ_ANONYMOUS_ROUTING, + // size: { + // xstr64: file.mSize.xstr64, + // }, + // })) && m.redraw() + // : ''; + // }, }, filesInfo[file.mHash] ? filesInfo[file.mHash].retval @@ -542,7 +773,7 @@ const PostView = () => { : ['Download', m('i.fas.fa-download')] : 'Please Wait...' ), - fileDown.list[file.mHash] && + fileDown.list[file.mHash] && // using the file from files_util to display download. m(fileUtil.File, { info: fileDown.list[file.mHash], direction: 'down', @@ -559,7 +790,7 @@ const PostView = () => { 'button', { onclick: () => { - util.popupmessage( + widget.popupMessage( m(AddComment, { parent_comment: '', channelId: v.attrs.channelId, @@ -572,6 +803,32 @@ const PostView = () => { }, 'Add Comment' ), + m( + 'label[for=idtags', + { + style: { marginLeft: '10px' }, + }, + '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( @@ -580,7 +837,9 @@ const PostView = () => { 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 @@ -598,4 +857,5 @@ const PostView = () => { module.exports = { ChannelView, PostView, + createchannel, }; diff --git a/webui-src/app/channels/channels.js b/webui-src/app/channels/channels.js index b4ef882..b103f17 100644 --- a/webui-src/app/channels/channels.js +++ b/webui-src/app/channels/channels.js @@ -3,22 +3,31 @@ const widget = require('widgets'); const rs = require('rswebui'); const util = require('channels/channels_util'); const viewUtil = require('channels/channel_view'); +const peopleUtil = require('people/people_util'); const getChannels = { All: [], PopularChannels: [], SubscribedChannels: [], MyChannels: [], + OtherChannels: [], async load() { const res = await rs.rsJsonApiRequest('/rsgxschannels/getChannelsSummaries'); const data = res.body; getChannels.All = data.channels; - getChannels.PopularChannels = getChannels.All; getChannels.SubscribedChannels = getChannels.All.filter( (channel) => channel.mSubscribeFlags === util.GROUP_SUBSCRIBE_SUBSCRIBED || - channel.mSubscribeFlags === util.GROUP_MY_CHANNEL + channel.mSubscribeFlags === util.GROUP_MY_CHANNEL // my channel is subscribed ); + // getChannels.PopularChannels = getChannels.All; + getChannels.PopularChannels = getChannels.All.filter( + (a) => !getChannels.SubscribedChannels.includes(a) + ); + getChannels.PopularChannels.sort((a, b) => b.mPop - a.mPop); + getChannels.OtherChannels = getChannels.PopularChannels.slice(5); + getChannels.PopularChannels = getChannels.PopularChannels.slice(0, 5); + getChannels.MyChannels = getChannels.All.filter( (channel) => channel.mSubscribeFlags === util.GROUP_MY_CHANNEL ); @@ -32,46 +41,74 @@ const sections = { OtherChannels: require('channels/other_channels'), }; -const Layout = { - oninit: () => { - rs.setBackgroundTask(getChannels.load, 5000, () => { - // return m.route.get() === '/files/files'; - }); - }, - // onupdate: getChannels.load, - view: (vnode) => - m('.tab-page', [ - Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mMsgId') - ? '' - : Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mGroupId') - ? m(util.SearchBar, { - category: 'posts', - channelId: vnode.attrs.pathInfo.mGroupId, - }) - : m(util.SearchBar, { - category: 'channels', - }), - m(widget.Sidebar, { - tabs: Object.keys(sections), - baseRoute: '/channels/', - }), - m( - '.channel-node-panel', +const Layout = () => { + let ownId; + return { + oninit: () => { + rs.setBackgroundTask(getChannels.load, 5000, () => { + // return m.route.get() === '/files/files'; + }); + peopleUtil.ownIds((data) => { + ownId = data; + for (let i = 0; i < ownId.length; i++) { + if (Number(ownId[i]) === 0) { + ownId.splice(i, 1); + } + } + ownId.unshift(0); // we need an extra check when a channel is created with no identity. + }); + }, + // onupdate: getChannels.load, + view: (vnode) => + m('.tab-page', [ Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mMsgId') - ? m(viewUtil.PostView, { - msgId: vnode.attrs.pathInfo.mMsgId, + ? '' + : Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mGroupId') + ? m(util.SearchBar, { + category: 'posts', channelId: vnode.attrs.pathInfo.mGroupId, }) - : Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mGroupId') - ? m(viewUtil.ChannelView, { - id: vnode.attrs.pathInfo.mGroupId, - }) - : m(sections[vnode.attrs.pathInfo.tab], { - list: getChannels[vnode.attrs.pathInfo.tab], - }) - ), - ]), + : m(util.SearchBar, { + category: 'channels', + }), + m( + 'button', + { + style: { fontSize: '1.2em', width: '200px' }, + onclick: () => + ownId && + widget.popupMessage( + m(viewUtil.createchannel, { + authorId: ownId, + }) + ), + }, + 'Create Channel' + ), + m(widget.Sidebar, { + tabs: Object.keys(sections), + baseRoute: '/channels/', + }), + m( + '.channel-node-panel', + + Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mMsgId') // posts + ? m(viewUtil.PostView, { + msgId: vnode.attrs.pathInfo.mMsgId, + channelId: vnode.attrs.pathInfo.mGroupId, + }) + : Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mGroupId') // channels view + ? m(viewUtil.ChannelView, { + id: vnode.attrs.pathInfo.mGroupId, + }) + : m(sections[vnode.attrs.pathInfo.tab], { + // subscribed, all, popular, other + list: getChannels[vnode.attrs.pathInfo.tab], + }) + ), + ]), + }; }; module.exports = { diff --git a/webui-src/app/channels/channels_util.js b/webui-src/app/channels/channels_util.js index ac106a8..7ddd264 100644 --- a/webui-src/app/channels/channels_util.js +++ b/webui-src/app/channels/channels_util.js @@ -1,22 +1,34 @@ const m = require('mithril'); const rs = require('rswebui'); -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 channels 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. +// rstypes.h:96 +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 channels 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_CHANNEL = GROUP_SUBSCRIBE_ADMIN + GROUP_SUBSCRIBE_SUBSCRIBED + GROUP_SUBSCRIBE_PUBLISH; + +// rsfiles.h:168 const RS_FILE_REQ_ANONYMOUS_ROUTING = 0x00000040; + +// rsgxscommon.h:194 const GXS_VOTE_DOWN = 0x0001; const GXS_VOTE_UP = 0x0002; +//rsgxscircles.h:50 +const PUBLIC = 1; /// Public distribution +const EXTERNAL = 2; /// Restricted to an external circle, based on GxsIds +const NODES_GROUP = 3; + const Data = { - DisplayChannels: {}, - Posts: {}, - Comments: {}, - TopComments: {}, - ParentCommentMap: {}, + DisplayChannels: {}, // chanID -> channel info + Posts: {}, // chanID, PostID -> {post, isSearched} + Comments: {}, // threadID, msgID -> {Comment, showReplies} + TopComments: {}, // threadID, msgID -> comment(Top thread comment) + ParentCommentMap: {}, // stores replies of a comment threadID, msgID -> comment + Votes: {}, }; async function updatecontent(content, channelid) { @@ -33,13 +45,15 @@ async function updatecontent(content, channelid) { Data.Comments[content.mThreadId][content.mMsgId] = { comment: res.body.comments[0], showReplies: false, - }; // Comments[post][comment] + }; // Comments[post][comment] const comm = res.body.comments[0]; if (Data.TopComments[comm.mMeta.mThreadId] === undefined) { Data.TopComments[comm.mMeta.mThreadId] = {}; } if (comm.mMeta.mThreadId === comm.mMeta.mParentId) { - Data.TopComments[comm.mMeta.mThreadId][comm.mMeta.mMsgId] = comm; // pushing top comments respective to post + // this is a check for the top level comments + Data.TopComments[comm.mMeta.mThreadId][comm.mMeta.mMsgId] = comm; + // pushing top comments respective to post } else { if (Data.ParentCommentMap[comm.mMeta.mParentId] === undefined) { Data.ParentCommentMap[comm.mMeta.mParentId] = {}; @@ -48,17 +62,23 @@ async function updatecontent(content, channelid) { } } else if (res.body.retval && res.body.votes.length > 0) { const vote = res.body.votes[0]; - if ( - Data.Comments[vote.mMeta.mThreadId] && - Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId] // finding [post][comment] - ) { - if (vote.mVoteType === GXS_VOTE_UP) { - Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId].comment.mUpVotes += 1; - } - if (vote.mVoteType === GXS_VOTE_DOWN) { - Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId].comment.mDownVotes += 1; - } - } + Data.Votes[vote.mMeta.mMsgId] = vote; + // console.log(vote); + // if ( + // Data.Comments[vote.mMeta.mThreadId] && + // Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId] // finding [post][comment] + // ) { + // console.log(Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId].comment); + + // if (vote.mVoteType === GXS_VOTE_UP) { + // console.log(Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId].comment.mUpVotes); + // Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId].comment.mUpVotes += 1; + // console.log(Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId].comment.mUpVotes); + // } + // if (vote.mVoteType === GXS_VOTE_DOWN) { + // Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId].comment.mDownVotes += 1; + // } + // } } } @@ -68,6 +88,7 @@ async function updatedisplaychannels(keyid, details) { }); details = res1.body.channelsInfo[0]; Data.DisplayChannels[keyid] = { + // struct for a channel name: details.mMeta.mGroupName, isSearched: true, description: details.mDescription, @@ -76,6 +97,7 @@ async function updatedisplaychannels(keyid, details) { isSubscribed: details.mMeta.mSubscribeFlags === GROUP_SUBSCRIBE_SUBSCRIBED || details.mMeta.mSubscribeFlags === GROUP_MY_CHANNEL, + mychannel: details.mMeta.mSubscribeFlags === GROUP_MY_CHANNEL, posts: details.mMeta.mVisibleMsgCount, activity: details.mMeta.mLastPost, created: details.mMeta.mPublishTs, @@ -89,10 +111,36 @@ async function updatedisplaychannels(keyid, details) { }); if (res2.body.retval) { - res2.body.summaries.map((content) => { - updatecontent(content, keyid); + res2.body.summaries.map(async (content) => { + await updatecontent(content, keyid); }); } + console.log(Data.Votes); + // Data.Votes.length > 1 && + Object.keys(Data.Votes).map((key, index) => { + console.log('hello'); + if ( + Data.Comments[Data.Votes[key].mMeta.mThreadId] && + Data.Comments[Data.Votes[key].mMeta.mThreadId][Data.Votes[key].mMeta.mParentId] // finding [post][comment] + ) { + console.log( + Data.Comments[Data.Votes[key].mMeta.mThreadId][Data.Votes[key].mMeta.mParentId].comment + ); + + if (Data.Votes[key].mVoteType === GXS_VOTE_UP) { + // console.log(Data.Comments[Data.Votes[key].mMeta.mThreadId][Data.Votes[key].mMeta.mParentId].comment.mUpVotes); + Data.Comments[Data.Votes[key].mMeta.mThreadId][ + Data.Votes[key].mMeta.mParentId + ].comment.mUpVotes += 1; + // console.log(Data.Comments[Data.Votes[key].mMeta.mThreadId][Data.Votes[key].mMeta.mParentId].comment.mUpVotes); + } + if (Data.Votes[key].mVoteType === GXS_VOTE_DOWN) { + Data.Comments[Data.Votes[key].mMeta.mThreadId][ + Data.Votes[key].mMeta.mParentId + ].comment.mDownVotes += 1; + } + } + }); } const DisplayChannelsFromList = () => { return { @@ -150,7 +198,6 @@ const CommentsTable = () => { }; }; - const FilesTable = () => { return { oninit: (v) => {}, @@ -163,6 +210,7 @@ const FilesTable = () => { }; function formatbytes(bytes, decimals = 2) { + // takes in size returns a string (size)(kb/gb/tb) if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; @@ -171,24 +219,6 @@ function formatbytes(bytes, decimals = 2) { return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } -function popupmessage(message) { - const container = document.getElementById('modal-container'); - container.style.display = 'block'; - m.render( - container, - m('.modal-content', [ - m( - 'button.red', - { - onclick: () => (container.style.display = 'none'), - }, - m('i.fas.fa-times') - ), - message, - ]) - ); -} - const ChannelTable = () => { return { oninit: (v) => {}, @@ -196,6 +226,7 @@ const ChannelTable = () => { }; }; const SearchBar = () => { + // same search bar is used for both channels and posts let searchString = ''; return { view: (v) => @@ -206,6 +237,7 @@ const SearchBar = () => { oninput: (e) => { searchString = e.target.value.toLowerCase(); if (v.attrs.category.localeCompare('channels') === 0) { + // for channels for (const hash in Data.DisplayChannels) { if (Data.DisplayChannels[hash].name.toLowerCase().indexOf(searchString) > -1) { Data.DisplayChannels[hash].isSearched = true; @@ -215,6 +247,7 @@ const SearchBar = () => { } } else { for (const hash in Data.Posts[v.attrs.channelId]) { + // for posts if ( Data.Posts[v.attrs.channelId][hash].post.mMeta.mMsgName .toLowerCase() @@ -234,7 +267,6 @@ const SearchBar = () => { module.exports = { Data, SearchBar, - popupmessage, ChannelSummary, formatbytes, DisplayChannelsFromList, @@ -249,5 +281,8 @@ module.exports = { GROUP_MY_CHANNEL, GXS_VOTE_DOWN, GXS_VOTE_UP, + PUBLIC, + EXTERNAL, + NODES_GROUP, RS_FILE_REQ_ANONYMOUS_ROUTING, }; diff --git a/webui-src/app/channels/other_channels.js b/webui-src/app/channels/other_channels.js index 6dfc91c..2cf5851 100644 --- a/webui-src/app/channels/other_channels.js +++ b/webui-src/app/channels/other_channels.js @@ -1,9 +1,32 @@ const m = require('mithril'); +const util = require('channels/channels_util'); const Layout = () => { return { - view: () => [m('.widget', [m('h2', 'Other Channels'), m('hr')])], + view: (v) => [ + m('.widget', [ + m('h3', 'Other Channels'), + m('hr'), + m( + util.ChannelTable, + m('tbody', [ + v.attrs.list.map((channel) => + m(util.ChannelSummary, { + details: channel, + category: 'OtherChannels', + }) + ), + v.attrs.list.map((channel) => + m(util.DisplayChannelsFromList, { + id: channel.mGroupId, + category: 'OtherChannels', + }) + ), + ]) + ), + ]), + ], }; }; -module.exports = Layout(); +module.exports = Layout; diff --git a/webui-src/app/config/config.css b/webui-src/app/config/config.css new file mode 100644 index 0000000..ce9879a --- /dev/null +++ b/webui-src/app/config/config.css @@ -0,0 +1,6 @@ +.config-node-panel { + position: relative; + bottom: 250px; + margin-left: 200px; + animation: fadein 0.5s; + } \ No newline at end of file diff --git a/webui-src/app/config/config_network.js b/webui-src/app/config/config_network.js index 9b84153..f6d821f 100644 --- a/webui-src/app/config/config_network.js +++ b/webui-src/app/config/config_network.js @@ -6,28 +6,16 @@ const util = require('config/config_util'); // for future use /* eslint-disable no-unused-vars */ const SetNwMode = () => { - let mode = undefined; - let sslId = undefined; - const setMode = () => rs.rsJsonApiRequest('/'); + let sslId; return { - oninit: util.getSslId().then((val) => { - sslId = val; - rs.rsJsonApiRequest('/'); - }), + oninit: async () => { + // const res = await rs.rsJsonApiRequest('/rsPeers/getOwnId'); + console.log(util.getSslId()); + }, view: () => [ m('p', 'Network mode:'), - m( - 'select', - { - oninput: (e) => (mode = e.target.value), - value: mode, - onchange: setMode, - }, - ['Discovery on(recommended)', 'Discovery off'].map((val, i) => - m('option[value=' + (i + 1) + ']', val) - ) - ), + ], }; }; @@ -130,7 +118,7 @@ const Component = () => { m('.grid-2col', [ m(SetLimits), m(SetOpMode), - // m(SetNwMode), + m(SetNwMode), ]), ]), }; diff --git a/webui-src/app/config/config_resolver.js b/webui-src/app/config/config_resolver.js index d03f162..38a6b2d 100644 --- a/webui-src/app/config/config_resolver.js +++ b/webui-src/app/config/config_resolver.js @@ -17,7 +17,7 @@ const Layout = { tabs: Object.keys(sections), baseRoute: '/config/', }), - m('.node-panel', vnode.children), + m('.config-node-panel', vnode.children), ]), }; diff --git a/webui-src/app/files/files.css b/webui-src/app/files/files.css index d97f314..a5be344 100644 --- a/webui-src/app/files/files.css +++ b/webui-src/app/files/files.css @@ -22,6 +22,14 @@ float: right; margin-left: 15px; } +.file-view select { + float: right; + margin-left: 15px; +} +.file-view label { + float: right; + margin-left: 15px; +} span > i { margin-right: 5px; } @@ -36,3 +44,37 @@ span > i { width: 40%; box-shadow: 0 0 3px #aaa; } +.file-node-panel { + position: relative; + bottom: 200px; + margin-left: 200px; + animation: fadein 0.5s; +} + +table.myfiles td { + word-wrap: break-word; +} +table.myfiles th:nth-child(1) { + width: 2%; +} +table.myfiles th:nth-child(2) { + width: 50%; +} +table.myfiles td:nth-child(2) { + text-align: start; +} +table.friendsfiles td { + word-wrap: break-word; +} +table.friendsfiles th:nth-child(1) { + width: 2%; +} +table.friendsfiles th:nth-child(2) { + width: 50%; +} +table.friendsfiles th:nth-child(4) { + width: 40%; +} +table.friendsfiles td:nth-child(2) { + text-align: start; +} diff --git a/webui-src/app/files/files_downloads.js b/webui-src/app/files/files_downloads.js index 0bf2fdd..42d0063 100644 --- a/webui-src/app/files/files_downloads.js +++ b/webui-src/app/files/files_downloads.js @@ -7,14 +7,18 @@ const Downloads = { statusMap: {}, hashes: [], - loadHashes() { - rs.rsJsonApiRequest('/rsFiles/FileDownloads', {}, (d) => (Downloads.hashes = d.hashs)); + async loadHashes() { + const res = await rs.rsJsonApiRequest( + '/rsFiles/FileDownloads', + {}, + (d) => (Downloads.hashes = d.hashs) + ); }, - loadStatus() { - Downloads.loadHashes(); + async loadStatus() { + await Downloads.loadHashes(); const fileKeys = Object.keys(Downloads.statusMap); - if (Downloads.hashes.length !== fileKeys.length) { + if (Downloads.hashes !== undefined && Downloads.hashes.length !== fileKeys.length) { // New file added if (Downloads.hashes.length > fileKeys.length) { const newHashes = util.compareArrays(Downloads.hashes, fileKeys); @@ -165,5 +169,6 @@ const Component = () => { module.exports = { Component, + Downloads, list: Downloads.statusMap, }; diff --git a/webui-src/app/files/files_resolver.js b/webui-src/app/files/files_resolver.js index 2159bb5..24345de 100644 --- a/webui-src/app/files/files_resolver.js +++ b/webui-src/app/files/files_resolver.js @@ -6,6 +6,8 @@ const downloads = require('files/files_downloads'); const uploads = require('files/files_uploads'); const util = require('files/files_util'); const search = require('files/files_search'); +const myfile = require('files/my_files'); +const friendfile = require('files/friends_files'); const MyFiles = () => { return { @@ -22,6 +24,8 @@ const MyFiles = () => { const sections = { files: MyFiles, search, + MyFiles: myfile, + FriendsFiles: friendfile, }; const Layout = { @@ -31,7 +35,7 @@ const Layout = { tabs: Object.keys(sections), baseRoute: '/files/', }), - m('.node-panel', vnode.children), + m('.file-node-panel', vnode.children), ]), }; diff --git a/webui-src/app/files/files_search.js b/webui-src/app/files/files_search.js index 9ec4d61..a261fb0 100644 --- a/webui-src/app/files/files_search.js +++ b/webui-src/app/files/files_search.js @@ -3,26 +3,33 @@ const rs = require('rswebui'); let matchString = ''; const SearchBar = () => { + let headers = {}; return { + oninit: () => { + headers['Accept'] = 'application/json'; + if (rs.loginKey.isVerified) { + headers['Authorization'] = 'Basic ' + btoa(rs.loginKey.username + ':' + rs.loginKey.passwd); + } + }, view: () => m('input[type=text][placeholder=search]', { value: matchString, oninput: (e) => (matchString = e.target.value), onchange: (e) => { console.log('searching for string: ', matchString); - // let source = new EventSource( - // 'http://127.0.0.1:9092/rsFiles/turtleSearchRequest', { - // withCredentials: true - // }); - rs.rsJsonApiRequest( - '/rsfiles/turtleSearchRequest', - { - matchString, - }, - (data, stat) => { - console.log('got: ', stat, ' data: ', data); - } - ); + let source = new EventSource('http://127.0.0.1:9092/rsFiles/turtleSearchRequest', { + headers, + // withCredentials: true, + }); + // rs.rsJsonApiRequest( + // '/rsfiles/turtleSearchRequest', + // { + // matchString, + // }, + // (data, stat) => { + // console.log('got: ', stat, ' data: ', data); + // } + // ); }, }), }; diff --git a/webui-src/app/files/files_util.js b/webui-src/app/files/files_util.js index e2d6b76..16e8a11 100644 --- a/webui-src/app/files/files_util.js +++ b/webui-src/app/files/files_util.js @@ -1,5 +1,6 @@ const m = require('mithril'); const rs = require('rswebui'); +const widget = require('widgets'); const RS_FILE_CTRL_PAUSE = 0x00000100; const RS_FILE_CTRL_START = 0x00000200; @@ -15,6 +16,7 @@ const FT_STATE_PAUSED = 0x0006; const FT_STATE_CHECKING_HASH = 0x0007; const RS_FILE_REQ_ANONYMOUS_ROUTING = 0x00000040; +const RS_FILE_HINTS_REMOTE = 0x00000008; function makeFriendlyUnit(bytes) { let cnt = bytes; @@ -52,7 +54,7 @@ function calcRemainingTime(bytes, rate) { } } -function fileAction(hash, action) { +async function fileAction(hash, action) { let actionHeader = ''; const jsonParams = { hash, @@ -82,7 +84,8 @@ function fileAction(hash, action) { console.error('Unknown action in Downloads.control()'); return; } - rs.rsJsonApiRequest(actionHeader, jsonParams, () => {}); + const res = await rs.rsJsonApiRequest(actionHeader, jsonParams, () => {}); + return res.body.retval; } function actionButton(file, action) { @@ -120,7 +123,28 @@ function actionButton(file, action) { title: 'cancel', onclick() { - fileAction(file.hash, 'cancel'); + widget.popupMessage( + m('Cancelpop', [ + m('p', 'Are you sure you want to cancel download?'), + m( + 'button', + { + onclick: async () => { + const res = await fileAction(file.hash, 'cancel'); + if (res) { + widget.popupMessage(m('p', 'Download Cancelled Successfully')); + } else { + widget.popupMessage(m('p', 'Download Cancel Failed')); + } + + m.redraw(); + }, + }, + 'Cancel' + ), + ]) + ); + // fileAction(file.hash, 'cancel'); }, }, m('i.fas.fa-times') @@ -151,7 +175,16 @@ const ProgressBar = () => { }; }; +const chunkStrats = [ + 'CHUNK_STRATEGY_STREAMING', + 'CHUNK_STRATEGY_RANDOM', + 'CHUNK_STRATEGY_PROGRESSIVE', +]; +const chunkStratsOptions = ['Streaming', 'Random', 'Progressive']; +// rstypes.h :: 366 + const File = () => { + let chunkStrat; return { view: (v) => m( @@ -172,12 +205,30 @@ const File = () => { v.attrs.info, v.attrs.info.downloadStatus === FT_STATE_PAUSED ? 'resume' : 'pause' ), + + m( + 'select[id=chunkTag]', + { + value: chunkStrat, + onchange: async (e) => { + chunkStrat = chunkStrats[e.target.selectedIndex]; + const res = await rs.rsJsonApiRequest('/rsFiles/setChunkStrategy', { + hash: v.attrs.info.hash, + newStrategy: chunkStrat, + }); + }, + }, + [chunkStratsOptions.map((opt) => m('option', { value: opt }, opt))] + ), + m('label[for=chunkTag]', 'Set Chunk Strategy: '), ], v.attrs.direction === 'up' ? [] : m(ProgressBar, { rate: (v.attrs.transferred / v.attrs.info.size.xint64) * 100, }), + m('span.filestat', m('i.fas.fa-download'), makeFriendlyUnit(v.attrs.transferred)), + m('span.filestat', m('i.fas.fa-file'), makeFriendlyUnit(v.attrs.info.size.xint64)), m( 'span.filestat', @@ -208,7 +259,7 @@ const SearchBar = () => { let searchString = ''; return { view: (v) => - m('input[type=text][placeholder=search].searchbar', { + m('input[type=text][placeholder=SearchDownloads].searchbar', { value: searchString, oninput: (e) => { searchString = e.target.value.toLowerCase(); @@ -232,6 +283,37 @@ function compareArrays(big, small) { return !this.has(val); }, new Set(small)); } +const MyFilesTable = () => { + return { + view: (v) => + m('table.myfiles', [ + m('tr', [m('th', ''), m('th', 'My Directories'), m('th', 'Size')]), + v.children, + ]), + }; +}; +const FriendsFilesTable = () => { + return { + view: (v) => + m('table.friendsfiles', [ + m('tr', [ + m('th', ''), + m('th', 'Friends Directories'), + m('th', 'Size'), + m('th', m('i.fas.fa-download')), + ]), + v.children, + ]), + }; +}; +function formatbytes(bytes, decimals = 2) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} module.exports = { RS_FILE_CTRL_PAUSE, @@ -246,8 +328,12 @@ module.exports = { FT_STATE_PAUSED, FT_STATE_CHECKING_HASH, RS_FILE_REQ_ANONYMOUS_ROUTING, + RS_FILE_HINTS_REMOTE, makeFriendlyUnit, File, SearchBar, compareArrays, + MyFilesTable, + FriendsFilesTable, + formatbytes, }; diff --git a/webui-src/app/files/friends_files.js b/webui-src/app/files/friends_files.js new file mode 100644 index 0000000..8676c15 --- /dev/null +++ b/webui-src/app/files/friends_files.js @@ -0,0 +1,173 @@ +const m = require('mithril'); +const rs = require('rswebui'); +const util = require('files/files_util'); +const widget = require('widgets'); +const fileDown = require('files/files_downloads'); + +function displayfiles() { + const childrenList = []; // stores children details + let loaded = false; // checks whether we have loaded the children details or not. + let parStruct; // stores current struct(details, showChild) + let isFile = false; + let haveFile = false; + let isId = false; + let nameOfId; + return { + oninit: async (v) => { + if (v.attrs.par_directory) { + parStruct = v.attrs.par_directory; + if (Number(parStruct.details.hash) !== 0) { + isFile = true; + const res = await rs.rsJsonApiRequest('/rsfiles/alreadyHaveFile', { + // checks if the file is already there with the user + hash: parStruct.details.hash, + }); + haveFile = res.body.retval; + } + } + if (v.attrs.replyDepth === 1 && parStruct) { + isId = true; + const res = await rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { + sslId: parStruct.details.name, + }); + if (res.body.retval) { + nameOfId = res.body.det.name; + } + } + }, + view: (v) => [ + m('tr', [ + parStruct && Object.keys(parStruct.details.children).length + ? m( + 'td', + m('i.fas.fa-angle-right', { + class: 'fa-rotate-' + (parStruct.showChild ? '90' : '0'), + style: 'margin-top:12px', + onclick: () => { + if (!loaded) { + // if it is not already retrieved. + parStruct.details.children.map(async (child) => { + const res = await rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { + handle: child.handle.xint64, + flags: util.RS_FILE_HINTS_REMOTE, + }); + childrenList.push(res.body.details); + loaded = true; + }); + } + parStruct.showChild = !parStruct.showChild; + }, + }) + ) + : m('td', ''), + m( + 'td', + { + style: { + position: 'relative', + '--replyDepth': v.attrs.replyDepth, + left: `calc(30px*${v.attrs.replyDepth})`, + }, + }, + isId + ? nameOfId + ' (' + parStruct.details.name.slice(0, 8) + '...)' + : parStruct.details.name + ), + m('td', util.formatbytes(parStruct.details.size.xint64)), + isFile && + m( + 'td', + // using the file from files_util to display download. + fileDown.list[parStruct.details.hash] + ? m(util.File, { + info: fileDown.list[parStruct.details.hash], + direction: 'down', + transferred: fileDown.list[parStruct.details.hash].transfered.xint64, + parts: [], + }) + : m( + 'button', + { + style: { fontSize: '0.9em' }, + onclick: async () => { + widget.popupMessage([ + m('p', 'Start Download?'), + m( + 'button', + { + onclick: async () => { + if (!haveFile) { + const res = await rs.rsJsonApiRequest('/rsFiles/FileRequest', { + fileName: parStruct.details.name, + hash: parStruct.details.hash, + flags: util.RS_FILE_REQ_ANONYMOUS_ROUTING, + size: { + xstr64: parStruct.details.size.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' + ), + ]); + }, + }, + + haveFile ? 'Open File' : ['Download', m('i.fas.fa-download')] + ) + ), + ]), + parStruct.showChild && // recursive call to show children + childrenList.map((child) => + m(displayfiles, { + par_directory: { details: child, showChild: false }, + replyDepth: v.attrs.replyDepth + 1, + }) + ), + ], + }; +} + +const Layout = () => { + // let root_handle; + let parent; + return { + oninit: async () => { + const res = await rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { + flags: util.RS_FILE_HINTS_REMOTE, + }); + parent = res; + }, + view: (v) => [ + m('.widget', [ + m('h3', 'Friends Files'), + m( + util.FriendsFilesTable, + m( + 'tbody', + parent && // root + m(displayfiles, { + par_directory: { details: parent.body.details, showChild: false }, + replyDepth: 0, + }) + ) + ), + ]), + ], + }; +}; + +module.exports = Layout; diff --git a/webui-src/app/files/my_files.js b/webui-src/app/files/my_files.js new file mode 100644 index 0000000..8e532af --- /dev/null +++ b/webui-src/app/files/my_files.js @@ -0,0 +1,90 @@ +const m = require('mithril'); +const rs = require('rswebui'); +const util = require('files/files_util'); + +function displayfiles() { + const childrenList = []; // stores children details + let loaded = false; // checks whether we have loaded the children details or not. + let parStruct; // stores current struct(details, showChild) + return { + oninit: (v) => { + if (v.attrs.par_directory) { + parStruct = v.attrs.par_directory; + } + }, + view: (v) => [ + m('tr', [ + parStruct && Object.keys(parStruct.details.children).length + ? m( + 'td', + m('i.fas.fa-angle-right', { + class: 'fa-rotate-' + (parStruct.showChild ? '90' : '0'), + style: 'margin-top:12px', + onclick: () => { + if (!loaded) { // if it is not already retrieved + parStruct.details.children.map(async (child) => { + const res = await rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { + handle: child.handle.xint64, + }); + childrenList.push(res.body.details); + loaded = true; + }); + } + parStruct.showChild = !parStruct.showChild; + }, + }) + ) + : m('td', ''), + m( + 'td', + { + style: { + position: 'relative', + '--replyDepth': v.attrs.replyDepth, + left: `calc(30px*${v.attrs.replyDepth})`, + + }, + }, + parStruct.details.name + ), + m('td', util.formatbytes(parStruct.details.size.xint64)), + ]), + parStruct.showChild && + childrenList.map((child) => + m(displayfiles, { // recursive call + par_directory: { details: child, showChild: false }, + replyDepth: v.attrs.replyDepth + 1, + }) + ), + ], + }; +} + +const Layout = () => { + // let root_handle; + let parent; + return { + oninit: async () => { + const res = await rs.rsJsonApiRequest('/rsfiles/requestDirDetails', {}); + parent = res; + }, + view: (v) => [ + m('.widget', [ + m('h3', 'My Files'), + m( + util.MyFilesTable, + m( + 'tbody', + parent && + m(displayfiles, { + par_directory: { details: parent.body.details, showChild: false }, + replyDepth: 0, + }) + ) + ), + ]), + ], + }; +}; + +module.exports = Layout; diff --git a/webui-src/app/forums/forum_view.js b/webui-src/app/forums/forum_view.js index 01881df..ab3abe3 100644 --- a/webui-src/app/forums/forum_view.js +++ b/webui-src/app/forums/forum_view.js @@ -1,78 +1,405 @@ const m = require('mithril'); const rs = require('rswebui'); const util = require('forums/forums_util'); +const peopleUtil = require('people/people_util'); +const { updatedisplayforums } = require('./forums_util'); -function displaythread() { +function createforum() { + let title; + let body; + let identity; return { - oninit: (v) => {}, - view: ({ attrs: { threadStruct, replyDepth } }) => { - const thread = threadStruct.thread; - let parMap = []; - if (util.Data.ParentThreadMap[thread.mMeta.mMsgId]) { - parMap = util.Data.ParentThreadMap[thread.mMeta.mMsgId]; + oninit: (vnode) => { + if (vnode.attrs.authorId) { + identity = vnode.attrs.authorId[0]; } - return [ - m('tr', [ - Object.keys(parMap).length - ? m( - 'td', - m('i.fas.fa-angle-right', { - class: 'fa-rotate-' + (threadStruct.showReplies ? '90' : '0'), - style: 'margin-top:12px', - onclick: () => { - threadStruct.showReplies = !threadStruct.showReplies; - }, - }) - ) - : m('td', ''), - - m( - 'td', - { - style: { - position: 'relative', - '--replyDepth': replyDepth, - left: 'calc(30px*var(--replyDepth))', - }, - onclick: () => (ThreadView.showThread = thread.mMeta.mMsgId), + }, + view: (vnode) => + m('.widget', [ + m('h3', 'Create Forum'), + m('hr'), + m('input[type=text][placeholder=Title]', { + oninput: (e) => (title = e.target.value), + }), + m('label[for=tags]', 'Select identity'), + m( + 'select[id=idtags]', + { + value: identity, + onchange: (e) => { + identity = vnode.attrs.authorId[e.target.selectedIndex]; }, - [thread.mMeta.mMsgName] - ), - m('td', ''), - m('td', rs.userList.userMap[thread.mMeta.mAuthorId]), - m( - 'td', - typeof thread.mMeta.mPublishTs === 'object' - ? new Date(thread.mMeta.mPublishTs.xint64 * 1000).toLocaleString() - : 'undefined' - ), - ]), - threadStruct.showReplies && + }, + [ + vnode.attrs.authorId && + vnode.attrs.authorId.map((o) => + m( + 'option', + { value: o }, + rs.userList.userMap[o] + ? rs.userList.userMap[o].toLocaleString() + ' (' + o.slice(0, 8) + '...)' + : 'No Signature' + ) + ), + ] + ), + m('textarea[rows=5][placeholder=Description]', { + style: { width: '90%', display: 'block' }, + oninput: (e) => (body = e.target.value), + value: body, + }), + m( + 'button', + { + onclick: async () => { + const res = await rs.rsJsonApiRequest('/rsgxsforums/createForumV2', { + name: title, + description: body, + ...(Number(identity) !== 0 && { authorId: identity }), // if id == '0', authorId is left empty + }); + if (res.body.retval) { + util.updatedisplayforums(res.body.forumId); + m.redraw(); + } + res.body.retval === false + ? util.popupmessage([m('h3', 'Error'), m('hr'), m('p', res.body.errorMessage)]) + : util.popupmessage([ + m('h3', 'Success'), + m('hr'), + m('p', 'Forum created successfully'), + ]); + }, + }, + 'Create' + ), + ]), + }; +} +const EditThread = () => { + let title = ''; + let body = ''; + return { + oninit: (vnode) => { + title = vnode.attrs.current_title; + body = vnode.attrs.current_body; + }, + view: (vnode) => + m('.widget', [ + m('h3', 'Edit Thread'), + m('hr'), + + m( + 'iddisplay', + { + style: { display: 'block ruby' }, // same line block ruby + }, + [ + 'Identity: ', + m('h5[id=authid]', rs.userList.userMap[vnode.attrs.authorId].toLocaleString()), + ] + ), + m( + 'titledisplay', + { + style: { display: 'block ruby' }, + }, + [ + 'Title: ', + m('input[type=text][placeholder=Title]', { + value: vnode.attrs.current_title, + oninput: (e) => (title = e.target.value), + }), + ] + ), + m('textarea[rows=5]', { + style: { width: '90%', display: 'block' }, + oninput: (e) => (body = e.target.value), + value: vnode.attrs.current_body, + }), + m( + 'button', + { + onclick: async () => { + const res = await rs.rsJsonApiRequest('/rsgxsforums/createPost', { + forumId: vnode.attrs.forumId, + mBody: body, + title: title, + authorId: vnode.attrs.authorId, + parentId: vnode.attrs.current_parent, + origPostId: vnode.attrs.current_msgid, + }); + res.body.retval === false + ? util.popupmessage([m('h3', 'Error'), m('hr'), m('p', res.body.errorMessage)]) + : util.popupmessage([ + m('h3', 'Success'), + m('hr'), + m('p', 'Thread edited successfully'), + ]); + util.updatedisplayforums(vnode.attrs.forumId); + m.redraw(); + }, + }, + 'Add' + ), + ]), + }; +}; +const AddThread = () => { + let title = ''; + let body = ''; + let identity; + return { + oninit: (vnode) => { + if (vnode.attrs.authorId) { + identity = vnode.attrs.authorId[0]; + } + }, + view: (vnode) => + m('.widget', [ + m('h3', 'Add Thread'), + m('hr'), + (vnode.attrs.parent_thread !== '') > 0 + ? [m('h5', 'Reply to thread: '), m('p', vnode.attrs.parent_thread)] + : '', + m('input[type=text][placeholder=Title]', { + oninput: (e) => (title = e.target.value), + }), + m('label[for=tags]', 'Select identity'), + m( + 'select[id=idtags]', + { + value: identity, + onchange: (e) => { + identity = vnode.attrs.authorId[e.target.selectedIndex]; + }, + }, + [ + vnode.attrs.authorId && + vnode.attrs.authorId.map((o) => + m( + 'option', + { value: o }, + rs.userList.userMap[o].toLocaleString() + ' (' + o.slice(0, 8) + '...)' + ) + ), + ] + ), + m('textarea[rows=5]', { + style: { width: '90%', display: 'block' }, + oninput: (e) => (body = e.target.value), + value: body, + }), + m( + 'button', + { + onclick: async () => { + const res = + (vnode.attrs.parent_thread !== '') > 0 // is it a reply or a new thread + ? await rs.rsJsonApiRequest('/rsgxsforums/createPost', { + forumId: vnode.attrs.forumId, + mBody: body, + title: title, + authorId: identity, + parentId: vnode.attrs.parentId, + }) + : await rs.rsJsonApiRequest('/rsgxsforums/createPost', { + forumId: vnode.attrs.forumId, + mBody: body, + title: title, + authorId: identity, + }); + + res.body.retval === false + ? util.popupmessage([m('h3', 'Error'), m('hr'), m('p', res.body.errorMessage)]) + : util.popupmessage([ + m('h3', 'Success'), + m('hr'), + m('p', 'Thread added successfully'), + ]); + util.updatedisplayforums(vnode.attrs.forumId); + m.redraw(); + }, + }, + 'Add' + ), + ]), + }; +}; +function displaythread() { + // recursive function to display all the threads + let groupmessagepair; + let unread; + let editpermission = false; + return { + view: (v) => { + const thread = v.attrs.threadStruct.thread; + groupmessagepair = { first: thread.mMeta.mGroupId, second: thread.mMeta.mOrigMsgId }; + let parMap = []; + if (util.Data.ParentThreadMap[thread.mMeta.mOrigMsgId]) { + parMap = util.Data.ParentThreadMap[thread.mMeta.mOrigMsgId]; + } + unread = thread.mMeta.mMsgStatus === util.THREAD_UNREAD; + v.attrs.identity && + v.attrs.identity.map((val) => { + if (val.localeCompare(thread.mMeta.mAuthorId) === 0) { + // if the author of the thread matches one of our own ids + editpermission = true; + } + }); + return [ + m( + 'tr', + { + style: unread ? { fontWeight: 'bold' } : '', + }, + [ + Object.keys(parMap).length // if this thread has some replies + ? m( + 'td', + m('i.fas.fa-angle-right', { + class: 'fa-rotate-' + (v.attrs.threadStruct.showReplies ? '90' : '0'), + style: 'margin-top:12px', + onclick: () => { + v.attrs.threadStruct.showReplies = !v.attrs.threadStruct.showReplies; + }, + }) + ) + : m('td', ''), + + m( + 'td', + { + style: { + position: 'relative', + '--replyDepth': v.attrs.replyDepth, + left: 'calc(30px*var(--replyDepth))', // shifts reply by 30 px + }, + onclick: async () => { + v.attrs.changeThread(thread.mMeta.mOrigMsgId); + if (unread) { + const res = await rs.rsJsonApiRequest('/rsgxsforums/markRead', { + messageId: groupmessagepair, + read: true, + }); + if (res.body.retval) { + updatedisplayforums(thread.mMeta.mGroupId); + m.redraw(); + } + } + }, + ondblclick: () => + (v.attrs.threadStruct.showReplies = !v.attrs.threadStruct.showReplies), + }, + [ + thread.mMeta.mMsgName, + m('options', { style: 'display:block' }, [ + m( + 'button', + { + style: 'font-size:15px', + onclick: () => + util.popupmessage( + m(AddThread, { + parent_thread: thread.mMeta.mMsgName, + forumId: thread.mMeta.mGroupId, + authorId: v.attrs.identity, + parentId: thread.mMeta.mMsgId, + }) + ), + }, + 'Reply' + ), + editpermission && + m( + 'button', + { + style: 'font-size:15px', + onclick: () => + util.popupmessage( + m(EditThread, { + current_thread: thread.mMeta.mMsgName, + forumId: thread.mMeta.mGroupId, + current_title: thread.mMeta.mMsgName, + current_body: thread.mMsg, + authorId: thread.mMeta.mAuthorId, + current_parent: thread.mMeta.mParentId, + current_msgid: thread.mMeta.mOrigMsgId, + }) + ), + }, + 'Edit' + ), + ]), + ] + ), + m( + 'td', + m( + 'button', + { + style: { fontSize: '15px' }, + onclick: async () => { + if (!unread) { + const res = await rs.rsJsonApiRequest('/rsgxsforums/markRead', { + messageId: groupmessagepair, + read: false, + }); + + if (res.body.retval) { + updatedisplayforums(thread.mMeta.mGroupId); + m.redraw(); + } + } + }, + }, + 'Mark Unread' + ) + ), + m('td', rs.userList.userMap[thread.mMeta.mAuthorId]), + m( + 'td', + typeof thread.mMeta.mPublishTs === 'object' + ? new Date(thread.mMeta.mPublishTs.xint64 * 1000).toLocaleString() + : 'undefined' + ), + ] + ), + v.attrs.threadStruct.showReplies && Object.keys(parMap).map((key, index) => m(displaythread, { - threadStruct: util.Data.Threads[parMap[key].mGroupId][parMap[key].mMsgId], - replyDepth: replyDepth + 1, + // recursive call to all replies + threadStruct: util.Data.Threads[parMap[key].mGroupId][parMap[key].mOrigMsgId], + replyDepth: v.attrs.replyDepth + 1, + identity: v.attrs.identity, + changeThread: v.attrs.changeThread, }) ), ]; }, }; } + const ThreadView = () => { let thread = {}; - + let ownId; return { showThread: '', - oninit: (v) => { + oninit: async (v) => { if ( util.Data.ParentThreads[v.attrs.forumId] && util.Data.ParentThreads[v.attrs.forumId][v.attrs.msgId] ) { thread = util.Data.ParentThreads[v.attrs.forumId][v.attrs.msgId]; - v.state.showThread = v.attrs.msgId; } + peopleUtil.ownIds((data) => { + ownId = data; + for (let i = 0; i < ownId.length; i++) { + if (Number(ownId[i]) === 0) { + ownId.splice(i, 1); + } + } + }); }, - onupdate: (v) => console.log(v.state.showThread), view: (v) => m('.widget', { key: v.attrs.msgId }, [ m( @@ -97,6 +424,11 @@ const ThreadView = () => { m(displaythread, { threadStruct: util.Data.Threads[v.attrs.forumId][v.attrs.msgId], replyDepth: 0, + identity: ownId, + changeThread(newThread) { + v.state.showThread = newThread; + // For displaying the messages of the threads. We pass this into the recursive function displaythreads() + }, }) ) ), @@ -106,6 +438,7 @@ const ThreadView = () => { util.Data.Threads[v.attrs.forumId] && util.Data.Threads[v.attrs.forumId][v.state.showThread] && m('p', m.trust(util.Data.Threads[v.attrs.forumId][v.state.showThread].thread.mMsg)), + // m.trust is to render html content directly. ], ]), }; @@ -118,6 +451,7 @@ const ForumView = () => { let createDate = {}; let lastActivity = {}; let topThreads = {}; + let ownId = ''; return { oninit: (v) => { if (util.Data.DisplayForums[v.attrs.id]) { @@ -136,6 +470,14 @@ const ForumView = () => { if (util.Data.ParentThreads[v.attrs.id]) { topThreads = util.Data.ParentThreads[v.attrs.id]; } + peopleUtil.ownIds((data) => { + ownId = data; + for (let i = 0; i < ownId.length; i++) { + if (Number(ownId[i]) === 0) { + ownId.splice(i, 1); + } + } + }); }, view: (v) => m( @@ -198,12 +540,22 @@ const ForumView = () => { style: 'display:' + (fsubscribed ? 'block' : 'none'), }, m('h3', 'Threads'), - m('button', - // { onclick: () => util.popupMessage() }, - [ - 'New Thread', - m('i.fas.fa-pencil-alt'), - ]), + m( + 'button', + { + onclick: () => { + util.popupmessage( + m(AddThread, { + parent_thread: '', + forumId: v.attrs.id, + authorId: ownId, + parentId: '', + }) + ); + }, + }, + ['New Thread', m('i.fas.fa-pencil-alt')] + ), m('hr'), m( util.ThreadsTable, @@ -213,11 +565,15 @@ const ForumView = () => { m( 'tr', { + style: + topThreads[key].mMsgStatus === util.THREAD_UNREAD + ? { fontWeight: 'bold' } + : '', onclick: () => { m.route.set('/forums/:tab/:mGroupId/:mMsgId', { tab: m.route.param().tab, mGroupId: v.attrs.id, - mMsgId: topThreads[key].mMsgId, + mMsgId: topThreads[key].mOrigMsgId, }); }, }, @@ -249,4 +605,5 @@ const ForumView = () => { module.exports = { ForumView, ThreadView, + createforum, }; diff --git a/webui-src/app/forums/forums.css b/webui-src/app/forums/forums.css index d9b87a4..2d7c5a8 100644 --- a/webui-src/app/forums/forums.css +++ b/webui-src/app/forums/forums.css @@ -66,3 +66,14 @@ table.threadreply td:nth-child(2) { table.threadreply td { word-wrap: break-word; } +table.threadreply tr:hover { + background-color: #eef3f6; + cursor: pointer; +} +#idtags{ + width: 160px; + text-align: center; + font-size: medium; + margin-left: 10px; + height: 40px; +} \ No newline at end of file diff --git a/webui-src/app/forums/forums.js b/webui-src/app/forums/forums.js index efb73f8..d6a75f0 100644 --- a/webui-src/app/forums/forums.js +++ b/webui-src/app/forums/forums.js @@ -3,6 +3,7 @@ const widget = require('widgets'); const rs = require('rswebui'); const util = require('forums/forums_util'); const viewUtil = require('forums/forum_view'); +const peopleUtil = require('people/people_util'); const getForums = { All: [], @@ -30,38 +31,64 @@ const sections = { OtherForums: require('forums/other_forums'), }; -const Layout = { - oninit: () => { - rs.setBackgroundTask(getForums.load, 5000, () => { - // return m.route.get() === '/files/files'; - }); - }, - view: (vnode) => - m('.tab-page', [ - m(util.SearchBar, { - list: getForums.All, - }), - m(widget.Sidebar, { - tabs: Object.keys(sections), - baseRoute: '/forums/', - }), - m( - '.forums-node-panel', +const Layout = () => { + let ownId; - Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mMsgId') - ? m(viewUtil.ThreadView, { - msgId: vnode.attrs.pathInfo.mMsgId, - forumId: vnode.attrs.pathInfo.mGroupId, - }) - : Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mGroupId') - ? m(viewUtil.ForumView, { - id: vnode.attrs.pathInfo.mGroupId, - }) - : m(sections[vnode.attrs.pathInfo.tab], { - list: getForums[vnode.attrs.pathInfo.tab], - }) - ), - ]), + return { + oninit: () => { + rs.setBackgroundTask(getForums.load, 5000, () => { + // return m.route.get() === '/files/files'; + }); + peopleUtil.ownIds((data) => { + ownId = data; + for (let i = 0; i < ownId.length; i++) { + if (Number(ownId[i]) === 0) { + ownId.splice(i, 1); + } + } + ownId.unshift(0); + }); + }, + view: (vnode) => + m('.tab-page', [ + m(util.SearchBar, { + list: getForums.All, + }), + m( + 'button', + { + style: { fontSize: '1.2em', width: '200px' }, + onclick: () => + util.popupmessage( + m(viewUtil.createforum, { + authorId: ownId, + }) + ), + }, + 'Create Forum' + ), + m(widget.Sidebar, { + tabs: Object.keys(sections), + baseRoute: '/forums/', + }), + m( + '.forums-node-panel', + + Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mMsgId') // thread's view + ? m(viewUtil.ThreadView, { + msgId: vnode.attrs.pathInfo.mMsgId, + forumId: vnode.attrs.pathInfo.mGroupId, + }) + : Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mGroupId') // Forum's view + ? m(viewUtil.ForumView, { + id: vnode.attrs.pathInfo.mGroupId, + }) + : m(sections[vnode.attrs.pathInfo.tab], { + list: getForums[vnode.attrs.pathInfo.tab], + }) + ), + ]), + }; }; module.exports = { diff --git a/webui-src/app/forums/forums_util.js b/webui-src/app/forums/forums_util.js index a734027..24545ac 100644 --- a/webui-src/app/forums/forums_util.js +++ b/webui-src/app/forums/forums_util.js @@ -7,6 +7,8 @@ const GROUP_SUBSCRIBE_SUBSCRIBED = 0x04; // means: you are subscribed to a group 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: {}, @@ -20,6 +22,7 @@ async function updatedisplayforums(keyid, details = {}) { }); details = res.body.forumsInfo[0]; Data.DisplayForums[keyid] = { + // struct for a forum name: details.mMeta.mGroupName, author: details.mMeta.mAuthorId, isSearched: true, @@ -43,19 +46,39 @@ async function updatedisplayforums(keyid, details = {}) { msgsIds: [thread.mMsgId], }); - if (res3.body.retval) { - Data.Threads[keyid][thread.mMsgId] = { thread: res3.body.msgs[0], showReplies: false }; - } - if (Data.ParentThreads[keyid] === undefined) { - Data.ParentThreads[keyid] = {}; - } - if (thread.mThreadId === thread.mParentId) { - Data.ParentThreads[keyid][thread.mMsgId] = thread; - } else { - if (Data.ParentThreadMap[thread.mParentId] === undefined) { - Data.ParentThreadMap[thread.mParentId] = {}; + if ( + res3.body.retval && + (Data.Threads[keyid][thread.mOrigMsgId] === undefined || + Data.Threads[keyid][thread.mOrigMsgId].thread.mMeta.mPublishTs.xint64 < + thread.mPublishTs.xint64) + // here we get the latest edited thread for each thread by comparing the publish time + ) { + Data.Threads[keyid][thread.mOrigMsgId] = { thread: res3.body.msgs[0], showReplies: false }; + if ( + Data.Threads[keyid][thread.mOrigMsgId] && + Data.Threads[keyid][thread.mOrigMsgId].thread.mMeta.mMsgStatus === THREAD_UNREAD + ) { + let parent = Data.Threads[keyid][thread.mOrigMsgId].thread.mMeta.mParentId; + while (Data.Threads[keyid][parent]) { + // to mark all parent threads of an inread thread + Data.Threads[keyid][parent].thread.mMeta.mMsgStatus = THREAD_UNREAD; + parent = Data.Threads[keyid][parent].thread.mMeta.mParentId; + } + } + + if (Data.ParentThreads[keyid] === undefined) { + Data.ParentThreads[keyid] = {}; + } + if (thread.mThreadId === thread.mParentId) { + // top level thread. + Data.ParentThreads[keyid][thread.mOrigMsgId] = + Data.Threads[keyid][thread.mOrigMsgId].thread.mMeta; + } else { + if (Data.ParentThreadMap[thread.mParentId] === undefined) { + Data.ParentThreadMap[thread.mParentId] = {}; + } + Data.ParentThreadMap[thread.mParentId][thread.mOrigMsgId] = thread; } - Data.ParentThreadMap[thread.mParentId][thread.mMsgId] = thread; } }); } @@ -148,6 +171,23 @@ const SearchBar = () => { }), }; }; +function popupmessage(message) { + const container = document.getElementById('modal-container'); + container.style.display = 'block'; + m.render( + container, + m('.modal-content', [ + m( + 'button.red', + { + onclick: () => (container.style.display = 'none'), + }, + m('i.fas.fa-times') + ), + message, + ]) + ); +} module.exports = { Data, @@ -157,9 +197,12 @@ module.exports = { ForumTable, ThreadsTable, ThreadsReplyTable, + popupmessage, + updatedisplayforums, GROUP_SUBSCRIBE_ADMIN, GROUP_SUBSCRIBE_NOT_SUBSCRIBED, GROUP_SUBSCRIBE_PUBLISH, GROUP_SUBSCRIBE_SUBSCRIBED, GROUP_MY_FORUM, + THREAD_UNREAD, }; diff --git a/webui-src/app/home.js b/webui-src/app/home.js index cbda0ef..b6675f8 100644 --- a/webui-src/app/home.js +++ b/webui-src/app/home.js @@ -203,7 +203,7 @@ function invalidCertPrompt() { widget.popupMessage([m('h3', 'Error'), m('hr'), m('p', 'Not a valid Retroshare certificate.')]); } -function confirmAddPrompt(details, cert) { +function confirmAddPrompt(details, cert, long) { widget.popupMessage([ m('i.fas.fa-user-plus'), m('h3', 'Make friend'), @@ -214,39 +214,73 @@ function confirmAddPrompt(details, cert) { m('li', 'Location: ' + details.location + '(' + details.id + ')'), m('li', details.isHiddenNode ? details.hiddenNodeAddress : details.extAddr), ]), - m( - 'button', - { - onclick: () => - rs.rsJsonApiRequest('/rsPeers/loadCertificateFromString', { cert }, (data) => { - if (data.retval) { - widget.popupMessage([ - m('h3', 'Successful'), - m('hr'), - m('p', 'Successfully added friend.'), - ]); - } else { - widget.popupMessage([ - m('h3', 'Error'), - m('hr'), - m('p', 'An error occoured during adding. Friend not added.'), - ]); - } - }), - }, - 'Finish' - ), + + long + ? m( + 'button', + { + onclick: async () =>{ + const res = await rs.rsJsonApiRequest('/rsPeers/loadCertificateFromString', { cert }); + if (res.body.retval) { + widget.popupMessage([ + m('h3', 'Successful'), + m('hr'), + m('p', 'Successfully added friend.'), + ]); + } else { + widget.popupMessage([ + m('h3', 'Error'), + m('hr'), + m('p', 'An error occoured during adding. Friend not added.'), + ]); + } + }, + }, + 'Finish' + ) + : m( + 'button', + { + onclick: async () => { + const res = await rs.rsJsonApiRequest('/rsPeers/addSslOnlyFriend', { + sslId: details.id, + pgpId: details.gpg_id, + }); + if (res.body.retval) { + widget.popupMessage([ + m('h3', 'Successful'), + m('hr'), + m('p', 'Successfully added friend.'), + ]); + } else { + widget.popupMessage([ + m('h3', 'Error'), + m('hr'), + m('p', 'An error occoured during adding. Friend not added.'), + ]); + } + }, + }, + 'Finish' + ), ]); } -function addFriendFromCert(cert) { - rs.rsJsonApiRequest('/rsPeers/loadDetailsFromStringCert', { cert }, (data) => { - if (!data.retval) { - invalidCertPrompt(); - return null; - } - confirmAddPrompt(data.certDetails, cert); - }); +async function addFriendFromCert(cert) { + const res = await rs.rsJsonApiRequest('/rsPeers/parseShortInvite', { invite: cert }); + + if (res.body.retval) { + console.log(res.body); + confirmAddPrompt(res.body.details, cert, false); + } else { + rs.rsJsonApiRequest('/rsPeers/loadDetailsFromStringCert', { cert }, (data) => { + if (!data.retval) { + invalidCertPrompt(); + return null; + } + confirmAddPrompt(data.certDetails, cert, true); + }); + } } const AddFriend = () => { diff --git a/webui-src/app/rswebui.js b/webui-src/app/rswebui.js index 773b297..200422d 100644 --- a/webui-src/app/rswebui.js +++ b/webui-src/app/rswebui.js @@ -266,4 +266,5 @@ module.exports = { logon, events: eventQueue.events, userList, + loginKey, };