diff --git a/webui-src/app/boards/board.css b/webui-src/app/boards/board.css new file mode 100644 index 0000000..7977a29 --- /dev/null +++ b/webui-src/app/boards/board.css @@ -0,0 +1,102 @@ +.board-node-panel { + position: relative; + bottom: 200px; + margin-left: 200px; + animation: fadein 0.5s; + } + +/* subject */ +table.boards th:nth-child(1) { + width: 50%; + text-align: start; +} +/* subject */ +table.boards td:nth-child(1) { + text-align: start; + /* Truncate text with '...' */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +table.boards tr:hover { + background-color: #eef3f6; + cursor: pointer; +} + +table.boards tr.hidden{ + display: none; +} + +#searchboard{ + position:relative; + margin-left: 250px; +} + +img.boardpic { + float: left; + height: 150px; + width:150px; + padding: 25px; +} +#boarddetails{ + position: relative; + margin-left: 150px; +} +.p{ + margin:0; +} + +#toggleunsub{ + position: relative; + background: gray; +} + +#grid{ + display: grid; + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 100px; +} + +.card { + width: 150px; + height: 150px; + border: 2px solid gray; + cursor: pointer; + text-align: center; +} +.card-img{ + width: 120px; + height: 120px; +} +.card-title{ + position: absolute; + bottom: 0px; +} + +/* subject */ +table.comments th{ + height: 40px; +} +table.comments th:nth-child(1) { + width: 40%; +} +table.comments td { + word-wrap: break-word; +} +table.comments td:nth-child(1){ + border: 3px solid lightskyblue; + border-radius: 25px; + +} +table.comments tr{ + height: 40px; +} + +#options{ + width: 100px; + text-align: center; + font-size: medium; + margin-left: 20px; + height: 40px; +} \ No newline at end of file diff --git a/webui-src/app/boards/board_view.js b/webui-src/app/boards/board_view.js new file mode 100644 index 0000000..d723b1e --- /dev/null +++ b/webui-src/app/boards/board_view.js @@ -0,0 +1,142 @@ +const m = require('mithril'); +const rs = require('rswebui'); +const util = require('boards/boards_util'); +const Data = util.Data; +const peopleUtil = require('people/people_util'); + +const BoardView = () => { + let bname = ''; + let bimage = ''; + let bauthor = ''; + let bsubscribed = {}; + let bposts = 0; + let plist = {}; + let createDate = {}; + let lastActivity = {}; + return { + oninit: (v) => { + if (Data.DisplayBoards[v.attrs.id]) { + bname = Data.DisplayBoards[v.attrs.id].name; + bimage = Data.DisplayBoards[v.attrs.id].image; + if (rs.userList.userMap[Data.DisplayBoards[v.attrs.id].author]) { + bauthor = rs.userList.userMap[Data.DisplayBoards[v.attrs.id].author]; + } else if (Number(Data.DisplayBoards[v.attrs.id].author) === 0) { + bauthor = 'No Contact Author'; + } else { + bauthor = 'Unknown'; + } + bsubscribed = Data.DisplayBoards[v.attrs.id].isSubscribed; + bposts = Data.DisplayBoards[v.attrs.id].posts; + createDate = Data.DisplayBoards[v.attrs.id].created; + lastActivity = Data.DisplayBoards[v.attrs.id].activity; + } + if (Data.Posts[v.attrs.id]) { + plist = Data.Posts[v.attrs.id]; + } + }, + view: (v) => + m( + '.widget', + { + key: v.attrs.id, + }, + [ + m( + 'a[title=Back]', + { + onclick: () => + m.route.set('/boards/:tab', { + tab: m.route.param().tab, + }), + }, + m('i.fas.fa-arrow-left') + ), + m('h3', bname), + + m( + 'button', + { + onclick: async () => { + const res = await rs.rsJsonApiRequest('/rsposted/subscribeToBoard', { + boardId: v.attrs.id, + subscribe: !bsubscribed, + }); + if (res.body.retval) { + bsubscribed = !bsubscribed; + Data.DisplayBoards[v.attrs.id].isSubscribed = bsubscribed; + } + }, + }, + bsubscribed ? 'Subscribed' : 'Subscribe' + ), + m('img.boardpic', { + src: 'data:image/png;base64,' + bimage.mData.base64, + }), + m('[id=boarddetails]', [ + m('p', m('b', 'Posts: '), bposts), + m( + 'p', + m('b', 'Date created: '), + typeof createDate === 'object' + ? new Date(createDate.xint64 * 1000).toLocaleString() + : 'undefined' + ), + m('p', m('b', 'Admin: '), bauthor), + m( + 'p', + m('b', 'Last activity: '), + typeof lastActivity === 'object' + ? new Date(lastActivity.xint64 * 1000).toLocaleString() + : 'undefined' + ), + ]), + m('hr'), + m('boarddesc', m('b', 'Description: '), Data.DisplayBoards[v.attrs.id].description), + m('hr'), + m( + 'postdetails', + { + style: 'display:' + (bsubscribed ? 'block' : 'none'), + }, + m('h3', 'Posts'), + + m( + '[id=grid]', + Object.keys(plist).map((key, index) => [ + m( + 'div', + { + class: 'card', + style: 'display: ' + (plist[key].isSearched? 'block': 'none'), + onclick: () => { + m.route.set('/boards/:tab/:mGroupId/:mMsgId', { + tab: m.route.param().tab, + mGroupId: v.attrs.id, + mMsgId: key, + }); + }, + }, + [ + m('img', { + class: 'card-img', + src: 'data:image/png;base64,' + plist[key].post.mThumbnail.mData.base64, + + alt: 'No Thumbnail', + }), + m('div', { class: 'card-info' }, [ + m('h4', { class: 'card-title' }, plist[key].post.mMeta.mMsgName), + ]), + ] + ), + ]) + ) + ), + ] + ), + }; +}; + + +module.exports = { + BoardView, +}; diff --git a/webui-src/app/boards/boards.js b/webui-src/app/boards/boards.js new file mode 100644 index 0000000..add63a1 --- /dev/null +++ b/webui-src/app/boards/boards.js @@ -0,0 +1,80 @@ +const m = require('mithril'); +const widget = require('widgets'); +const rs = require('rswebui'); +const util = require('boards/boards_util'); +const viewUtil = require('boards/board_view'); + +const getBoards = +{ + All : [], + PopularBoards : [], + SubscribedBoards : [], + MyBoards : [], + async load(){ + const res = await rs.rsJsonApiRequest('/rsPosted/getBoardsSummaries'); + const data = res.body; + getBoards.All = data.groupInfo; + getBoards.PopularBoards = getBoards.All; + getBoards.SubscribedBoards = getBoards.All.filter( + (board) => + board.mSubscribeFlags === util.GROUP_SUBSCRIBE_SUBSCRIBED + ); + getBoards.MyBoards = getBoards.All.filter( + (board) => board.mSubscribeFlags === util.GROUP_MY_BOARD + ); + }, +}; + +const sections = { + MyBoards: require('boards/my_boards'), + SubscribedBoards: require('boards/subscribed_boards'), + PopularBoards: require('boards/popular_boards'), + OtherBoards: require('boards/other_boards') +}; + +const Layout = { + oninit : () => { + rs.setBackgroundTask(getBoards.load, 5000, () => { + }); + }, + // onupdate: getBoards.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', + boardId: vnode.attrs.pathInfo.mGroupId + }) + : m(util.SearchBar, { + category: 'boards' + }), + m(widget.Sidebar, { + tabs: Object.keys(sections), + baseRoute: '/boards/', + }), + m('.board-node-panel', + Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mMsgId') + ? m(viewUtil.PostView, { + msgId: vnode.attrs.pathInfo.mMsgId, + boardId: vnode.attrs.pathInfo.mGroupId, + }) + : Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mGroupId') + ? m(viewUtil.BoardView, { + id: vnode.attrs.pathInfo.mGroupId, + }) + : m(sections[vnode.attrs.pathInfo.tab], { + list: getBoards[vnode.attrs.pathInfo.tab], + }) + ), + ]), +}; + +module.exports = { + view: (vnode) => { + return m(Layout, { + pathInfo: vnode.attrs, + }); + }, +}; diff --git a/webui-src/app/boards/boards_util.js b/webui-src/app/boards/boards_util.js new file mode 100644 index 0000000..956530f --- /dev/null +++ b/webui-src/app/boards/boards_util.js @@ -0,0 +1,159 @@ +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. +const GROUP_SUBSCRIBE_NOT_SUBSCRIBED = 0x08; +const GROUP_MY_BOARD = GROUP_SUBSCRIBE_ADMIN + GROUP_SUBSCRIBE_SUBSCRIBED + GROUP_SUBSCRIBE_PUBLISH; +const GXS_VOTE_DOWN = 0x0001; +const GXS_VOTE_UP = 0x0002; + +const Data = { + DisplayBoards: {}, + Posts: {}, + Comments: {}, +}; + +async function updateContent(content, boardid) { + const res = await rs.rsJsonApiRequest('/rsPosted/getBoardContent', { + boardId: boardid, + contentsIds: [content.mMsgId], + }); + if (res.body.retval && res.body.posts.length > 0) { + Data.Posts[boardid][content.mMsgId] = { post: res.body.posts[0], isSearched: true }; + } else if (res.body.retval && res.body.comments.length > 0) { + if (Data.Comments[content.mThreadId] === undefined) { + Data.Comments[content.mThreadId] = {}; + } + Data.Comments[content.mThreadId][content.mMsgId] = res.body.comments[0]; + } 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] + ) { + if (vote.mVoteType === GXS_VOTE_UP) { + Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId].mUpVotes += 1; + } + if (vote.mVoteType === GXS_VOTE_DOWN) { + Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId].mDownVotes += 1; + } + } + } +} + +async function updateDisplayBoards(keyid, details) { + const res1 = await rs.rsJsonApiRequest('/rsPosted/getBoardsInfo', { + boardsIds: [keyid], + }); + details = res1.body.boardsInfo[0]; + Data.DisplayBoards[keyid] = { + name: details.mMeta.mGroupName, + isSearched: true, + description: details.mDescription, + image: details.mGroupImage, + author: details.mMeta.mAuthorId, + isSubscribed: + details.mMeta.mSubscribeFlags === GROUP_SUBSCRIBE_SUBSCRIBED || + details.mMeta.mSubscribeFlags === GROUP_MY_BOARD, + posts: details.mMeta.mVisibleMsgCount, + activity: details.mMeta.mLastPost, + created: details.mMeta.mPublishTs, + all: details, + }; + + if (Data.Posts[keyid] === undefined) { + Data.Posts[keyid] = {}; + } + + /*const res2 = await rs.rsJsonApiRequest('/rsPosted/getContentSummaries', { + boardId: keyid, + }); + + if (res2.body.retval) { + res2.body.summaries.map((content) => { + updateContent(content, keyid); + }); + }*/ + +} + +const DisplayBoardsFromList = () => { + return { + oninit: (v) => {}, + view: (v) => + m( + 'tr', + { + key: v.attrs.id, + class: + Data.DisplayBoards[v.attrs.id] && Data.DisplayBoards[v.attrs.id].isSearched + ? '' + : 'hidden', + onclick: () => { + m.route.set('/boards/:tab/:mGroupId', { + tab: v.attrs.category, + mGroupId: v.attrs.id, + }); + }, + }, + [m('td', Data.DisplayBoards[v.attrs.id] ? Data.DisplayBoards[v.attrs.id].name : '')] + ), + }; +}; + +const BoardSummary = () => { + let keyid = {}; + return { + oninit: (v) => { + keyid = v.attrs.details.mGroupId; + updateDisplayBoards(keyid); + }, + + view: (v) => {}, + }; +}; + +const BoardTable = () => { + return { + oninit: (v) => {}, + view: (v) => m('table.boards', [m('tr', [m('th', 'Board Name')]), v.children]), + }; +}; + +const SearchBar = () => { + let searchString = ''; + return { + view: (v) => + m('input[type=text][id=searchboard][placeholder=Search Subject].searchbar', { + value: searchString, + oninput: (e) => { + searchString = e.target.value.toLowerCase(); + for (const hash in Data.DisplayBoards) { + if (Data.DisplayBoards[hash].name.toLowerCase().indexOf(searchString) > -1) { + Data.DisplayBoards[hash].isSearched = true; + } else { + Data.DisplayBoards[hash].isSearched = false; + } + } + }, + }), + }; +}; + +module.exports = { + Data, + SearchBar, + BoardSummary, + DisplayBoardsFromList, + updateDisplayBoards, + BoardTable, + GROUP_SUBSCRIBE_ADMIN, + GROUP_SUBSCRIBE_NOT_SUBSCRIBED, + GROUP_SUBSCRIBE_PUBLISH, + GROUP_SUBSCRIBE_SUBSCRIBED, + GROUP_MY_BOARD, + GXS_VOTE_DOWN, + GXS_VOTE_UP, +}; diff --git a/webui-src/app/boards/my_boards.js b/webui-src/app/boards/my_boards.js new file mode 100644 index 0000000..5b69777 --- /dev/null +++ b/webui-src/app/boards/my_boards.js @@ -0,0 +1,32 @@ +const m = require('mithril'); +const util = require('boards/boards_util'); + +const Layout = () => { + return { + view: (v) => [ + m('.widget', [ + m('h3', 'My Boards'), + m('hr'), + m( + util.BoardTable, + m('tbody', [ + v.attrs.list.map((board) => + m(util.BoardSummary, { + details: board, + category: 'MyBoards', + }) + ), + v.attrs.list.map((board) => + m(util.DisplayBoardsFromList, { + id: board.mGroupId, + category: 'MyBoards', + }) + ), + ]) + ), + ]), + ], + }; +}; + +module.exports = Layout(); diff --git a/webui-src/app/boards/other_boards.js b/webui-src/app/boards/other_boards.js new file mode 100644 index 0000000..5c813d4 --- /dev/null +++ b/webui-src/app/boards/other_boards.js @@ -0,0 +1,11 @@ +const m = require('mithril'); + +const Layout = () => { + return{ + view: () => [ + m('.widget', [m('h2', 'Other Boards'), m('hr'), ]) + ] + }; +}; + +module.exports = Layout(); diff --git a/webui-src/app/boards/popular_boards.js b/webui-src/app/boards/popular_boards.js new file mode 100644 index 0000000..d027f1a --- /dev/null +++ b/webui-src/app/boards/popular_boards.js @@ -0,0 +1,33 @@ +const m = require('mithril'); +const util = require('boards/boards_util'); + +const Layout = () => { + return { + view: (v) => [ + m('.widget', [ + m('h3', 'Popular Boards'), + m('hr'), + m( + util.BoardTable, + m('tbody', [ + v.attrs.list.map((board) => + m(util.BoardSummary, { + details: board, + category: 'PopularBoards', + }), + ), + v.attrs.list.map((board) => + m(util.DisplayBoardsFromList, { + id: board.mGroupId, + category: 'PopularBoards', + }), + ), + ]) + ), + ]), + ], + }; +}; + +module.exports = Layout; + diff --git a/webui-src/app/boards/subscribed_boards.js b/webui-src/app/boards/subscribed_boards.js new file mode 100644 index 0000000..1338211 --- /dev/null +++ b/webui-src/app/boards/subscribed_boards.js @@ -0,0 +1,33 @@ +const m = require('mithril'); +const util = require('boards/boards_util'); + +const Layout = () => { + return { + view: (v) => [ + m('.widget', [ + m('h3', 'Subscribed Boards'), + m('hr'), + m( + util.BoardTable, + m('tbody', [ + v.attrs.list.map((board) => + m(util.BoardSummary, { + details: board, + category: 'SubscribedBoards', + }), + ), + v.attrs.list.map((board) => + m(util.DisplayBoardsFromList, { + id: board.mGroupId, + category: 'SubscribedBoards', + }), + ), + ]) + ), + ]), + ], + }; +}; + +module.exports = Layout; + diff --git a/webui-src/app/main.js b/webui-src/app/main.js index c7d9ce6..936d096 100644 --- a/webui-src/app/main.js +++ b/webui-src/app/main.js @@ -9,6 +9,7 @@ const mail = require('mail/mail_resolver'); const files = require('files/files_resolver'); const channels = require('channels/channels'); const forums = require('forums/forums'); +const boards = require('boards/boards'); const config = require('config/config_resolver'); const navIcon = { @@ -20,6 +21,7 @@ const navIcon = { files: m('i.fas.fa-folder-open.sidenav-icon'), channels: m('i.fas.fa-tv.sidenav-icon'), forums: m('i.fas.fa-bullhorn.sidenav-icon'), + boards: m('i.fas.fa-globe.sidenav-icon'), config: m('i.fas.fa-cogs.sidenav-icon'), }; @@ -56,7 +58,8 @@ const Layout = () => { mail: '/mail/inbox', files: '/files/files', channels: '/channels/MyChannels', - forums: 'forums/MyForums', + forums: '/forums/MyForums', + boards: '/boards/MyBoards', config: '/config/network', }, }), @@ -112,6 +115,15 @@ m.route(document.getElementById('main'), '/', { '/forums/:tab/:mGroupId': { render: (v) => m(Layout, m(forums, v.attrs)), }, + '/boards/:tab': { + render: (v) => m(Layout, m(boards, v.attrs)), + }, + '/boards/:tab/:mGroupId': { + render: (v) => m(Layout, m(boards, v.attrs)), + }, + '/boards/:tab/:mGroupId/:mMsgId': { + render: (v) => m(Layout, m(boards, v.attrs)), + }, '/config/:tab': { render: (v) => m(Layout, m(config, v.attrs)), },