├── .gitignore ├── CMakeLists.txt ├── README.md ├── webui-src ├── .editorconfig ├── .eslintrc.json ├── .prettierrc ├── app │ ├── boards │ │ ├── board_view.js │ │ ├── boards.js │ │ ├── boards_util.js │ │ ├── my_boards.js │ │ ├── other_boards.js │ │ ├── popular_boards.js │ │ └── subscribed_boards.js │ ├── channels │ │ ├── channel_view.js │ │ ├── channels.js │ │ ├── channels_util.js │ │ ├── my_channels.js │ │ ├── other_channels.js │ │ ├── popular_channels.js │ │ ├── sha1.js │ │ └── subscribed_channels.js │ ├── chat │ │ └── chat.js │ ├── config │ │ ├── config_files.js │ │ ├── config_mail.js │ │ ├── config_network.js │ │ ├── config_node.js │ │ ├── config_people.js │ │ ├── config_resolver.js │ │ ├── config_services.js │ │ └── config_util.js │ ├── files │ │ ├── files_downloads.js │ │ ├── files_manager.js │ │ ├── files_proxy.js │ │ ├── files_resolver.js │ │ ├── files_search.js │ │ ├── files_uploads.js │ │ ├── files_util.js │ │ ├── friends_files.js │ │ └── my_files.js │ ├── forums │ │ ├── forum_view.js │ │ ├── forums.js │ │ ├── forums_util.js │ │ ├── my_forums.js │ │ ├── other_forums.js │ │ ├── popular_forums.js │ │ └── subscribed_forums.js │ ├── home.js │ ├── login.js │ ├── mail │ │ ├── mail_attachment.js │ │ ├── mail_compose.js │ │ ├── mail_draftbox.js │ │ ├── mail_important.js │ │ ├── mail_inbox.js │ │ ├── mail_later.js │ │ ├── mail_outbox.js │ │ ├── mail_personal.js │ │ ├── mail_resolver.js │ │ ├── mail_sentbox.js │ │ ├── mail_spam.js │ │ ├── mail_starred.js │ │ ├── mail_system.js │ │ ├── mail_todo.js │ │ ├── mail_trashbox.js │ │ ├── mail_util.js │ │ └── mail_work.js │ ├── main.js │ ├── mithril.js │ ├── network │ │ ├── network.js │ │ └── network_data.js │ ├── people │ │ ├── people.js │ │ ├── people_own_contacts.js │ │ ├── people_ownids.js │ │ ├── people_resolver.js │ │ └── people_util.js │ ├── rswebui.js │ ├── scss │ │ ├── abstracts │ │ │ ├── _colors.scss │ │ │ ├── _functions.scss │ │ │ ├── _index.scss │ │ │ ├── _mixins.scss │ │ │ ├── _typography.scss │ │ │ └── _variables.scss │ │ ├── base │ │ │ ├── _base.scss │ │ │ ├── _index.scss │ │ │ └── _reset.scss │ │ ├── components │ │ │ ├── _buttons.scss │ │ │ ├── _index.scss │ │ │ ├── _media.scss │ │ │ ├── _navbar.scss │ │ │ ├── _posts.scss │ │ │ └── _progress-bar.scss │ │ ├── fontface │ │ │ ├── _Bold.scss │ │ │ ├── _BoldItalic.scss │ │ │ ├── _Italic.scss │ │ │ ├── _Light.scss │ │ │ ├── _LightItalic.scss │ │ │ ├── _Medium.scss │ │ │ ├── _MediumItalic.scss │ │ │ ├── _Regular.scss │ │ │ └── _index.scss │ │ ├── layouts │ │ │ ├── _index.scss │ │ │ ├── _modal-container.scss │ │ │ ├── _notification-container.scss │ │ │ └── _widget.scss │ │ ├── main.scss │ │ ├── pages │ │ │ ├── _board.scss │ │ │ ├── _channel.scss │ │ │ ├── _chat.scss │ │ │ ├── _config.scss │ │ │ ├── _files.scss │ │ │ ├── _forums.scss │ │ │ ├── _home.scss │ │ │ ├── _index.scss │ │ │ ├── _login.scss │ │ │ ├── _mail.scss │ │ │ ├── _network.scss │ │ │ └── _people.scss │ │ └── vendors │ │ │ ├── _fontawesome.scss │ │ │ ├── _index.scss │ │ │ └── _solid.scss │ └── widgets.js ├── assets │ ├── images │ │ ├── retroshare.svg │ │ └── user.svg │ └── webfonts │ │ ├── Roboto-Bold.ttf │ │ ├── Roboto-Bold.woff │ │ ├── Roboto-Bold.woff2 │ │ ├── Roboto-BoldItalic.ttf │ │ ├── Roboto-BoldItalic.woff │ │ ├── Roboto-BoldItalic.woff2 │ │ ├── Roboto-Italic.ttf │ │ ├── Roboto-Italic.woff │ │ ├── Roboto-Italic.woff2 │ │ ├── Roboto-Light.ttf │ │ ├── Roboto-Light.woff │ │ ├── Roboto-Light.woff2 │ │ ├── Roboto-LightItalic.ttf │ │ ├── Roboto-LightItalic.woff │ │ ├── Roboto-LightItalic.woff2 │ │ ├── Roboto-Medium.ttf │ │ ├── Roboto-Medium.woff │ │ ├── Roboto-Medium.woff2 │ │ ├── Roboto-MediumItalic.ttf │ │ ├── Roboto-MediumItalic.woff │ │ ├── Roboto-MediumItalic.woff2 │ │ ├── Roboto-Regular.ttf │ │ ├── Roboto-Regular.woff │ │ ├── Roboto-Regular.woff2 │ │ ├── fa-solid-900.eot │ │ ├── fa-solid-900.svg │ │ ├── fa-solid-900.ttf │ │ ├── fa-solid-900.woff │ │ └── fa-solid-900.woff2 ├── index.html ├── make-src │ ├── .prettierignore │ ├── README.md │ ├── build.bat │ ├── build.sh │ ├── init.bat │ ├── init.sh │ └── template.js ├── package.json ├── pnpm-lock.yaml └── styles.css └── webui.pro /.gitignore: -------------------------------------------------------------------------------- 1 | .qmake.stash 2 | Makefile 3 | webui/* 4 | webui.pro.user 5 | libwebui.so* 6 | node_modules/ 7 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # RetroShare decentralized communication platform 2 | # 3 | # Copyright (C) 2022 Gioacchino Mazzurco 4 | # Copyright (C) 2022 Asociación Civil Altermundi 5 | # 6 | # SPDX-License-Identifier: CC0-1.0 7 | 8 | cmake_minimum_required (VERSION 3.18.0) 9 | project(retroshare-webui) 10 | 11 | set( 12 | RS_DATA_DIR 13 | "${CMAKE_INSTALL_PREFIX}/share/retroshare" 14 | CACHE PATH 15 | "Path where to install RetroShare system wide data" ) 16 | 17 | option( 18 | RS_DEVELOPMENT_BUILD 19 | "Enable verbose build log. Userful just for development purposes." 20 | ON ) 21 | 22 | if(RS_DEVELOPMENT_BUILD) 23 | set(CMAKE_VERBOSE_MAKEFILE ON) 24 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) 25 | endif(RS_DEVELOPMENT_BUILD) 26 | 27 | set(WEBUI_SRC_DIR "${CMAKE_CURRENT_SOURCE_DIR}/webui-src") 28 | set(WEBUI_BUILD_DIR "${CMAKE_CURRENT_SOURCE_DIR}/webui") 29 | 30 | if(UNIX) 31 | add_custom_target( 32 | ${PROJECT_NAME} ALL 33 | COMMAND ./build.sh 34 | WORKING_DIRECTORY "${WEBUI_SRC_DIR}/make-src" 35 | COMMENT "Compiling RetroShare WebUI" 36 | SOURCES "${WEBUI_SRC_DIR}" ) 37 | 38 | install(DIRECTORY "${WEBUI_BUILD_DIR}" DESTINATION "${RS_DATA_DIR}") 39 | endif(UNIX) 40 | 41 | # TODO: Windows build/install 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Web Interface for Retroshare 2 | 3 | A web-based frontend for [Retroshare](https://github.com/Retroshare/Retroshare) 4 | which communicates with the client through the JSON API. 5 | 6 | ## Requirements 7 | 8 | - Retroshare v0.6.5+ with JSON API enabled(see instructions below) 9 | - A modern JavaScript-enabled web browser 10 | - [`qmake`](https://doc.qt.io/qt-5/qmake-manual.html)(optional) 11 | 12 | ## Installation 13 | 14 | > **Note:** The Web Interface is shipped by default in the latest release of 15 | > Retroshare. If you want to customise it or [contribute](#contributing) to it 16 | > then proceed with the following steps. 17 | 18 | ### Install WebUI 19 | 20 | First, you need to download and install the web interface javascript code 21 | itself: 22 | 23 | 1. **Clone the repo**: 24 | You can clone using git, or download the zip file and extract it 25 | 26 | ```bash 27 | git clone https://github.com/Retroshare/RSNewWebUI 28 | cd RSNewWebUI 29 | ``` 30 | 31 | 2. **Build the files**: 32 | If you have `qmake` installed, you just need to run it in the base directory: 33 | 34 | ```bash 35 | qmake . 36 | ``` 37 | 38 | If you do not have `qmake`, go to `webui-src/make-src/` and run the build 39 | script. Note that qmake is enough to do the build. 40 | 41 | ##### On Linux/MacOS: 42 | 43 | ```bash 44 | cd webui-src/make-src 45 | sh build.sh 46 | ``` 47 | 48 | ##### On Windows: 49 | 50 | ```bash 51 | cd webui-src\make-src\ 52 | ./build.bat 53 | ``` 54 | 55 | ### Compile Retroshare with JSON API 56 | 57 | If you are on older versions of Retroshare then it needs to be compiled with 58 | non-default options as follows: 59 | 60 | ```bash 61 | qmake CONFIG+="debug rs_jsonapi rs_webui" 62 | make 63 | ``` 64 | 65 | See the [RetroShare repo](https://github.com/Retroshare/Retroshare) for more 66 | detailed instructions on compiling RetroShare. You should afterwards see a tab 67 | 'JSON API' and a tab 'Web Interface' in the **Preferences**. 68 | 69 | ### Enable JSON API 70 | 71 | You need to enable the JSON API, through which the web interface communicates 72 | with the client: 73 | 74 | 1. Open Retroshare, go to `Preferences > JSON API`. 75 | 2. Make sure the **Enable Retroshare JSON API Server** box is checked. 76 | 77 | ### Enable Web Interface 78 | 79 | 1. Go to `Preferences > Webinterface`. 80 | 2. Make sure the **Enable Retroshare WEB Interface** box is checked. 81 | 3. Enter a password to protect access to the web interface. 82 | 83 | If necessary, point the **Web interface directory** to the place where the webui 84 | files are compiled. This is usually `RSNewWebUI/webui/`. 85 | 86 | In any case, click on "Apply settings" after making the changes. If everything 87 | goes ok, you should see a new token `webui:[your password]` under the 88 | **Authenticated Tokens** section in the **JSON API** preferences page. 89 | 90 | ## Usage 91 | 92 | ### Basic Usage 93 | 94 | This is the default link to access the WebInterface. 95 |
96 | Open this link your browser -> 97 | [https://localhost:9092/index.html](https://localhost:9092/index.html). 98 | 99 | > Note: If you changed the port in the JSON API preferences pages, the port 100 | > in the above line needs to be changed accordingly. 101 | 102 | ### Advanced Usage 103 | 104 | The Web interface is only accessible from localhost (127.0.0.1). If you want to 105 | access the web interface of a headless retroshare server, then you need to 106 | create a SSH tunnel as follows: 107 | 108 | ``` 109 | ssh login@server -L 9092:localhost:9092 -N 110 | ``` 111 | 112 | After that, the Web interface of the Retroshare running on 'server' is tunneled 113 | to your local machine and accessible through localhost:9092. 114 | 115 | Running a headless retroshare server is one possibility. The Webinterface 116 | however does not allow you to create new nodes. Therefore the steps are: 117 | 118 | 1. Create a node using the standard Qt UI. That can be done in another machine. 119 | 2. Copy the retroshare data directory (.retroshare/ on linux) on the server. 120 | 3. On the server, launch a headless retroshare using that node: 121 | ``` 122 | ./retroshare-service/src/retroshare-service -U list -W 123 | ``` 124 | 125 | After that follow instructions to launch your profile (you need to choose a 126 | webui password and enter the ID and login password of your node). 127 | 128 | ## Contributing 129 | 130 | For contributing, It is recommended that you read this entire section to have a 131 | better idea. 132 | 133 | ### Setup WebUI for contributing 134 | 135 | Follow these steps to setup the project and make it ready for 136 | contribution/customisation : 137 | 138 | - Fork and Clone this repository to your local machine. 139 | - `cd` into the cloned repo: 140 | ``` 141 | cd RSNewWebUI 142 | ``` 143 | - Run this command to install the dependecies for the project. 144 | ``` 145 | pnpm install 146 | ``` 147 | 148 | ### Run the WebUI 149 | 150 | - Run this command to watch for any changes in the `scss` files and compile them 151 | to css. 152 | 153 | ``` 154 | pnpm watch 155 | ``` 156 | 157 | - You can now start to edit the source code. But you must run the below command 158 | to see the changes reflected in the browser everytime you edit the code for 159 | webui which is in the `webui-src/app/` directory. 160 | 161 | ``` 162 | qmake . 163 | ``` 164 | 165 | ### Linting and Formatting 166 | 167 | Linting and formatting can be done with editor/IDE plugins. 168 | 169 | Or to do it manually, 170 | 171 | Install `prettier` and `eslint` (required for linting & formatting code): 172 | 173 | ```sh 174 | npm install -g prettier eslint 175 | ``` 176 | 177 | Next, run the following in the `webui-src` directory: 178 | 179 | To run the linter: `eslint app` 180 | 181 | To run the formatter: `prettier -c app` 182 | 183 | ### References 184 | 185 | Now, While contributing you can checkout these resources as you might need to 186 | look up for these often. 187 | 188 | - [mithril](https://mithril.js.org/hyperscript.html) 189 | - You can list files with @jsonapi in libretroshare/src/retroshare of 190 | [retroshare](https://github.com/RetroShare/RetroShare): 191 | 192 | ``` 193 | grep -c "@jsonapi" *.h|grep -v ":0" 194 | ``` 195 | 196 |
197 | 198 | And, that's it. You are more than welcome to contribute to this project. If you 199 | have any questions/difficulties in setting up or running the project, you can 200 | raise an issue and we will be more than willing to help you out. 201 | 202 | ### Bug Reports & Feature requests 203 | 204 | Please create an [issue](https://github.com/Retroshare/RsNewWebUI/issues) 205 | concisely describing the bug you faced, or the feature you would like to see 206 | implemented. 207 | 208 | ### Development 209 | 210 | Whether you are a JavaScript developer or a Web designer, you can help make the 211 | web interface better. Get in touch with us on the Developer forums in 212 | Retroshare. 213 | -------------------------------------------------------------------------------- /webui-src/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org/ 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /webui-src/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly", 12 | "window": "readonly", 13 | "document": "readonly" 14 | }, 15 | "parserOptions": { 16 | "ecmaVersion": 2020 17 | }, 18 | "ignorePatterns": ["mithril.js", "assets/*", "make-src/*"], 19 | "rules": { 20 | "linebreak-style": ["off", "unix"], 21 | "quotes": ["error", "single"], 22 | "semi": ["error", "always"], 23 | "arrow-spacing": ["error", { "before": true, "after": true }], 24 | "arrow-parens": ["error", "always"], 25 | "comma-style": ["error", "last"], 26 | "comma-spacing": ["error", { "before": false, "after": true }], 27 | "camelcase": [ 28 | "error", 29 | { 30 | "allow": ["^UNSAFE_"], 31 | "properties": "never", 32 | "ignoreGlobals": true 33 | } 34 | ], 35 | "eol-last": "error", 36 | "eqeqeq": ["error", "always", { "null": "ignore" }], 37 | "no-irregular-whitespace": ["error"], 38 | "no-trailing-spaces": ["error"], 39 | "no-unexpected-multiline": ["error"], 40 | "no-unreachable": ["error"], 41 | "no-var": "warn", 42 | "no-unused-vars": [ 43 | "error", 44 | { 45 | "args": "none", 46 | "caughtErrors": "none", 47 | "ignoreRestSiblings": true, 48 | "vars": "all" 49 | } 50 | ], 51 | "no-use-before-define": ["warn", { "functions": true, "classes": false }], 52 | "object-shorthand": "error", 53 | "prefer-const": ["error", { "destructuring": "all" }], 54 | "semi-style": ["warn", "last"], 55 | "spaced-comment": ["warn", "always"] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /webui-src/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "printWidth": 100, 4 | "tabWidth": 2, 5 | "useTabs": false, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /webui-src/app/boards/boards.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const widget = require('widgets'); 3 | const rs = require('rswebui'); 4 | const util = require('boards/boards_util'); 5 | const viewUtil = require('boards/board_view'); 6 | const peopleUtil = require('people/people_util'); 7 | 8 | const getBoards = { 9 | All: [], 10 | PopularBoards: [], 11 | SubscribedBoards: [], 12 | MyBoards: [], 13 | OtherBoards: [], 14 | async load() { 15 | const res = await rs.rsJsonApiRequest('/rsPosted/getBoardsSummaries'); 16 | const data = res.body; 17 | getBoards.All = data.groupInfo; 18 | getBoards.PopularBoards = getBoards.All; 19 | getBoards.PopularBoards.sort((a, b) => b.mPop - a.mPop); 20 | getBoards.OtherBoards = getBoards.PopularBoards.slice(5); 21 | getBoards.PopularBoards = getBoards.PopularBoards.slice(0, 5); 22 | getBoards.SubscribedBoards = getBoards.All.filter( 23 | (board) => board.mSubscribeFlags === util.GROUP_SUBSCRIBE_SUBSCRIBED 24 | ); 25 | getBoards.MyBoards = getBoards.All.filter( 26 | (board) => board.mSubscribeFlags === util.GROUP_MY_BOARD 27 | ); 28 | }, 29 | }; 30 | 31 | const sections = { 32 | MyBoards: require('boards/my_boards'), 33 | SubscribedBoards: require('boards/subscribed_boards'), 34 | PopularBoards: require('boards/popular_boards'), 35 | OtherBoards: require('boards/other_boards'), 36 | }; 37 | 38 | const Layout = () => { 39 | let ownId; 40 | 41 | return { 42 | oninit: () => { 43 | rs.setBackgroundTask(getBoards.load, 5000, () => { 44 | // return m.route.get() === '/files/files'; 45 | }); 46 | peopleUtil.ownIds((data) => { 47 | ownId = data; 48 | for (let i = 0; i < ownId.length; i++) { 49 | if (Number(ownId[i]) === 0) { 50 | ownId.splice(i, 1); 51 | } 52 | } 53 | ownId.unshift(0); 54 | }); 55 | }, 56 | view: (vnode) => 57 | m('.widget', [ 58 | m('.top-heading', [ 59 | m( 60 | 'button', 61 | { 62 | onclick: () => 63 | ownId && 64 | util.popupmessage( 65 | m(viewUtil.createboard, { 66 | authorId: ownId, 67 | }) 68 | ), 69 | }, 70 | 'Create Board' 71 | ), 72 | m(util.SearchBar, { 73 | list: getBoards.All, 74 | }), 75 | ]), 76 | Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mMsgId') 77 | ? m(viewUtil.PostView, { 78 | msgId: vnode.attrs.pathInfo.mMsgId, 79 | forumId: vnode.attrs.pathInfo.mGroupId, 80 | }) 81 | : Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mGroupId') 82 | ? m(viewUtil.BoardView, { 83 | id: vnode.attrs.pathInfo.mGroupId, 84 | }) 85 | : m(sections[vnode.attrs.pathInfo.tab], { 86 | list: getBoards[vnode.attrs.pathInfo.tab], 87 | }), 88 | ]), 89 | }; 90 | }; 91 | 92 | module.exports = { 93 | view: (vnode) => { 94 | return [ 95 | m(widget.Sidebar, { 96 | tabs: Object.keys(sections), 97 | baseRoute: '/boards/', 98 | }), 99 | m('.node-panel', m(Layout, { pathInfo: vnode.attrs })), 100 | ]; 101 | }, 102 | }; 103 | -------------------------------------------------------------------------------- /webui-src/app/boards/boards_util.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | 4 | const GROUP_SUBSCRIBE_ADMIN = 0x01;// means: you have the admin key for this group 5 | 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. 6 | const GROUP_SUBSCRIBE_SUBSCRIBED = 0x04;// means: you are subscribed to a group, which makes you a source for this group to your friend nodes. 7 | const GROUP_SUBSCRIBE_NOT_SUBSCRIBED = 0x08; 8 | const GROUP_MY_BOARD = GROUP_SUBSCRIBE_ADMIN + GROUP_SUBSCRIBE_SUBSCRIBED + GROUP_SUBSCRIBE_PUBLISH; 9 | const GXS_VOTE_DOWN = 0x0001; 10 | const GXS_VOTE_UP = 0x0002; 11 | 12 | //rsgxscircles.h:50 13 | const PUBLIC = 1; /// Public distribution 14 | const EXTERNAL = 2; /// Restricted to an external circle, based on GxsIds 15 | const NODES_GROUP = 3; 16 | 17 | const Data = { 18 | DisplayBoards: {}, // boardID -> board info 19 | Posts: {}, // boardID, PostID -> {post, isSearched} 20 | Comments: {}, // threadID, msgID -> {Comment, showReplies} 21 | }; 22 | 23 | async function updateContent(content, boardid) { 24 | const res = await rs.rsJsonApiRequest('/rsPosted/getBoardContent', { 25 | boardId: boardid, 26 | contentsIds: [content.mMsgId], 27 | }); 28 | if (res.body.retval && res.body.posts.length > 0) { 29 | Data.Posts[boardid][content.mMsgId] = { post: res.body.posts[0], isSearched: true }; 30 | } else if (res.body.retval && res.body.comments.length > 0) { 31 | if (Data.Comments[content.mThreadId] === undefined) { 32 | Data.Comments[content.mThreadId] = {}; 33 | } 34 | Data.Comments[content.mThreadId][content.mMsgId] = res.body.comments[0]; 35 | } else if (res.body.retval && res.body.votes.length > 0) { 36 | const vote = res.body.votes[0]; 37 | if ( 38 | Data.Comments[vote.mMeta.mThreadId] && 39 | Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId] 40 | ) { 41 | if (vote.mVoteType === GXS_VOTE_UP) { 42 | Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId].mUpVotes += 1; 43 | } 44 | if (vote.mVoteType === GXS_VOTE_DOWN) { 45 | Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId].mDownVotes += 1; 46 | } 47 | } 48 | } 49 | } 50 | 51 | async function updateDisplayBoards(keyid, details) { 52 | const res1 = await rs.rsJsonApiRequest('/rsPosted/getBoardsInfo', { 53 | boardsIds: [keyid], 54 | }); 55 | details = res1.body.boardsInfo[0]; 56 | Data.DisplayBoards[keyid] = { 57 | name: details.mMeta.mGroupName, 58 | isSearched: true, 59 | description: details.mDescription, 60 | image: details.mGroupImage, 61 | author: details.mMeta.mAuthorId, 62 | isSubscribed: 63 | details.mMeta.mSubscribeFlags === GROUP_SUBSCRIBE_SUBSCRIBED || 64 | details.mMeta.mSubscribeFlags === GROUP_MY_BOARD, 65 | posts: details.mMeta.mVisibleMsgCount, 66 | activity: details.mMeta.mLastPost, 67 | created: details.mMeta.mPublishTs, 68 | all: details, 69 | }; 70 | 71 | if (Data.Posts[keyid] === undefined) { 72 | Data.Posts[keyid] = {}; 73 | } 74 | 75 | /*const res2 = await rs.rsJsonApiRequest('/rsPosted/getContentSummaries', { 76 | boardId: keyid, 77 | }); 78 | 79 | if (res2.body.retval) { 80 | res2.body.summaries.map((content) => { 81 | updateContent(content, keyid); 82 | }); 83 | }*/ 84 | 85 | } 86 | 87 | const DisplayBoardsFromList = () => { 88 | return { 89 | oninit: (v) => {}, 90 | view: (v) => 91 | m( 92 | 'tr', 93 | { 94 | key: v.attrs.id, 95 | class: 96 | Data.DisplayBoards[v.attrs.id] && Data.DisplayBoards[v.attrs.id].isSearched 97 | ? '' 98 | : 'hidden', 99 | onclick: () => { 100 | m.route.set('/boards/:tab/:mGroupId', { 101 | tab: v.attrs.category, 102 | mGroupId: v.attrs.id, 103 | }); 104 | }, 105 | }, 106 | [m('td', Data.DisplayBoards[v.attrs.id] ? Data.DisplayBoards[v.attrs.id].name : '')] 107 | ), 108 | }; 109 | }; 110 | 111 | const BoardSummary = () => { 112 | let keyid = {}; 113 | return { 114 | oninit: (v) => { 115 | keyid = v.attrs.details.mGroupId; 116 | updateDisplayBoards(keyid); 117 | }, 118 | 119 | view: (v) => {}, 120 | }; 121 | }; 122 | 123 | const BoardTable = () => { 124 | return { 125 | oninit: (v) => {}, 126 | view: (v) => m('table.boards', [m('tr', [m('th', 'Board Name')]), v.children]), 127 | }; 128 | }; 129 | 130 | function popupmessage(message) { 131 | const container = document.getElementById('modal-container'); 132 | container.style.display = 'block'; 133 | m.render( 134 | container, 135 | m('.modal-content[id=composepopup]', [ 136 | m( 137 | 'button.red', 138 | { 139 | onclick: () => (container.style.display = 'none'), 140 | }, 141 | m('i.fas.fa-times') 142 | ), 143 | message, 144 | ]) 145 | ); 146 | } 147 | 148 | const SearchBar = () => { 149 | let searchString = ''; 150 | return { 151 | view: (v) => 152 | m('input[type=text][id=searchboard][placeholder=Search Subject].searchbar', { 153 | value: searchString, 154 | oninput: (e) => { 155 | searchString = e.target.value.toLowerCase(); 156 | for (const hash in Data.DisplayBoards) { 157 | if (Data.DisplayBoards[hash].name.toLowerCase().indexOf(searchString) > -1) { 158 | Data.DisplayBoards[hash].isSearched = true; 159 | } else { 160 | Data.DisplayBoards[hash].isSearched = false; 161 | } 162 | } 163 | }, 164 | }), 165 | }; 166 | }; 167 | 168 | module.exports = { 169 | Data, 170 | SearchBar, 171 | popupmessage, 172 | BoardSummary, 173 | DisplayBoardsFromList, 174 | updateDisplayBoards, 175 | BoardTable, 176 | GROUP_SUBSCRIBE_ADMIN, 177 | GROUP_SUBSCRIBE_NOT_SUBSCRIBED, 178 | GROUP_SUBSCRIBE_PUBLISH, 179 | GROUP_SUBSCRIBE_SUBSCRIBED, 180 | GROUP_MY_BOARD, 181 | GXS_VOTE_DOWN, 182 | GXS_VOTE_UP, 183 | PUBLIC, 184 | EXTERNAL, 185 | NODES_GROUP, 186 | }; 187 | -------------------------------------------------------------------------------- /webui-src/app/boards/my_boards.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('boards/boards_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'My Boards')), 8 | m('.widget__body', [ 9 | m( 10 | util.BoardTable, 11 | m('tbody', [ 12 | v.attrs.list.map((board) => 13 | m(util.BoardSummary, { 14 | details: board, 15 | category: 'MyBoards', 16 | }) 17 | ), 18 | v.attrs.list.map((board) => 19 | m(util.DisplayBoardsFromList, { 20 | id: board.mGroupId, 21 | category: 'MyBoards', 22 | }) 23 | ), 24 | ]) 25 | ), 26 | ]), 27 | ], 28 | }; 29 | }; 30 | 31 | module.exports = Layout(); 32 | -------------------------------------------------------------------------------- /webui-src/app/boards/other_boards.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('boards/boards_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Other Boards')), 8 | m('.widget__body', [ 9 | m( 10 | util.BoardTable, 11 | m('tbody', [ 12 | v.attrs.list.map((board) => 13 | m(util.BoardSummary, { 14 | details: board, 15 | category: 'OtherBoards', 16 | }) 17 | ), 18 | v.attrs.list.map((board) => 19 | m(util.DisplayBoardsFromList, { 20 | id: board.mGroupId, 21 | category: 'OtherBoards', 22 | }) 23 | ), 24 | ]) 25 | ), 26 | ]), 27 | ], 28 | }; 29 | }; 30 | 31 | module.exports = Layout(); 32 | -------------------------------------------------------------------------------- /webui-src/app/boards/popular_boards.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('boards/boards_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Popular Boards')), 8 | m('.widget__body', [ 9 | m( 10 | util.BoardTable, 11 | m('tbody', [ 12 | v.attrs.list.map((board) => 13 | m(util.BoardSummary, { 14 | details: board, 15 | category: 'PopularBoards', 16 | }) 17 | ), 18 | v.attrs.list.map((board) => 19 | m(util.DisplayBoardsFromList, { 20 | id: board.mGroupId, 21 | category: 'PopularBoards', 22 | }) 23 | ), 24 | ]) 25 | ), 26 | ]), 27 | ], 28 | }; 29 | }; 30 | 31 | module.exports = Layout; 32 | -------------------------------------------------------------------------------- /webui-src/app/boards/subscribed_boards.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('boards/boards_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Subscribed Boards')), 8 | m('.widget__body', [ 9 | m( 10 | util.BoardTable, 11 | m('tbody', [ 12 | v.attrs.list.map((board) => 13 | m(util.BoardSummary, { 14 | details: board, 15 | category: 'SubscribedBoards', 16 | }) 17 | ), 18 | v.attrs.list.map((board) => 19 | m(util.DisplayBoardsFromList, { 20 | id: board.mGroupId, 21 | category: 'SubscribedBoards', 22 | }) 23 | ), 24 | ]) 25 | ), 26 | ]), 27 | ], 28 | }; 29 | }; 30 | 31 | module.exports = Layout; 32 | -------------------------------------------------------------------------------- /webui-src/app/channels/channels.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const widget = require('widgets'); 3 | const rs = require('rswebui'); 4 | const util = require('channels/channels_util'); 5 | const viewUtil = require('channels/channel_view'); 6 | const peopleUtil = require('people/people_util'); 7 | 8 | const getChannels = { 9 | All: [], 10 | PopularChannels: [], 11 | SubscribedChannels: [], 12 | MyChannels: [], 13 | OtherChannels: [], 14 | async load() { 15 | const res = await rs.rsJsonApiRequest('/rsgxschannels/getChannelsSummaries'); 16 | const data = res.body; 17 | getChannels.All = data.channels; 18 | getChannels.SubscribedChannels = getChannels.All.filter( 19 | (channel) => 20 | channel.mSubscribeFlags === util.GROUP_SUBSCRIBE_SUBSCRIBED || 21 | channel.mSubscribeFlags === util.GROUP_MY_CHANNEL // my channel is subscribed 22 | ); 23 | // getChannels.PopularChannels = getChannels.All; 24 | getChannels.PopularChannels = getChannels.All.filter( 25 | (a) => !getChannels.SubscribedChannels.includes(a) 26 | ); 27 | getChannels.PopularChannels.sort((a, b) => b.mPop - a.mPop); 28 | getChannels.OtherChannels = getChannels.PopularChannels.slice(5); 29 | getChannels.PopularChannels = getChannels.PopularChannels.slice(0, 5); 30 | 31 | getChannels.MyChannels = getChannels.All.filter( 32 | (channel) => channel.mSubscribeFlags === util.GROUP_MY_CHANNEL 33 | ); 34 | }, 35 | }; 36 | 37 | const sections = { 38 | MyChannels: require('channels/my_channels'), 39 | SubscribedChannels: require('channels/subscribed_channels'), 40 | PopularChannels: require('channels/popular_channels'), 41 | OtherChannels: require('channels/other_channels'), 42 | }; 43 | 44 | const Layout = () => { 45 | let ownId; 46 | 47 | return { 48 | oninit: () => { 49 | rs.setBackgroundTask(getChannels.load, 5000, () => { 50 | // return m.route.get() === '/files/files'; 51 | }); 52 | peopleUtil.ownIds((data) => { 53 | ownId = data; 54 | for (let i = 0; i < ownId.length; i++) { 55 | if (Number(ownId[i]) === 0) { 56 | ownId.splice(i, 1); 57 | } 58 | } 59 | ownId.unshift(0); // we need an extra check when a channel is created with no identity. 60 | }); 61 | }, 62 | // onupdate: getChannels.load, 63 | view: (vnode) => 64 | m('.widget', [ 65 | m('.top-heading', [ 66 | m( 67 | 'button', 68 | { 69 | onclick: () => 70 | ownId && 71 | widget.popupMessage( 72 | m(viewUtil.createchannel, { 73 | authorId: ownId, 74 | }) 75 | ), 76 | }, 77 | 'Create Channel' 78 | ), 79 | Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mMsgId') 80 | ? '' 81 | : Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mGroupId') 82 | ? m(util.SearchBar, { 83 | category: 'posts', 84 | channelId: vnode.attrs.pathInfo.mGroupId, 85 | }) 86 | : m(util.SearchBar, { 87 | category: 'channels', 88 | }), 89 | ]), 90 | Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mMsgId') // posts 91 | ? m(viewUtil.PostView, { 92 | msgId: vnode.attrs.pathInfo.mMsgId, 93 | channelId: vnode.attrs.pathInfo.mGroupId, 94 | }) 95 | : Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mGroupId') // channels view 96 | ? m(viewUtil.ChannelView, { 97 | id: vnode.attrs.pathInfo.mGroupId, 98 | }) 99 | : m(sections[vnode.attrs.pathInfo.tab], { 100 | // subscribed, all, popular, other 101 | list: getChannels[vnode.attrs.pathInfo.tab], 102 | }), 103 | ]), 104 | }; 105 | }; 106 | 107 | module.exports = { 108 | view: (vnode) => { 109 | return [ 110 | m(widget.Sidebar, { 111 | tabs: Object.keys(sections), 112 | baseRoute: '/channels/', 113 | }), 114 | m('.node-panel', m(Layout, { pathInfo: vnode.attrs })), 115 | ]; 116 | }, 117 | }; 118 | -------------------------------------------------------------------------------- /webui-src/app/channels/my_channels.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('channels/channels_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'My Channels')), 8 | m('.widget__body', [ 9 | m( 10 | util.ChannelTable, 11 | m('tbody', [ 12 | v.attrs.list.map((channel) => 13 | m(util.ChannelSummary, { 14 | details: channel, 15 | category: 'MyChannels', 16 | }) 17 | ), 18 | v.attrs.list.map((channel) => 19 | m(util.DisplayChannelsFromList, { 20 | id: channel.mGroupId, 21 | category: 'MyChannels', 22 | }) 23 | ), 24 | ]) 25 | ), 26 | ]), 27 | ], 28 | }; 29 | }; 30 | 31 | module.exports = Layout; 32 | -------------------------------------------------------------------------------- /webui-src/app/channels/other_channels.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('channels/channels_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Other Channels')), 8 | m('.widget__body', [ 9 | m( 10 | util.ChannelTable, 11 | m('tbody', [ 12 | v.attrs.list.map((channel) => 13 | m(util.ChannelSummary, { 14 | details: channel, 15 | category: 'OtherChannels', 16 | }) 17 | ), 18 | v.attrs.list.map((channel) => 19 | m(util.DisplayChannelsFromList, { 20 | id: channel.mGroupId, 21 | category: 'OtherChannels', 22 | }) 23 | ), 24 | ]) 25 | ), 26 | ]), 27 | ], 28 | }; 29 | }; 30 | 31 | module.exports = Layout; 32 | -------------------------------------------------------------------------------- /webui-src/app/channels/popular_channels.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('channels/channels_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Popular Channels')), 8 | m('.widget__body', [ 9 | m( 10 | util.ChannelTable, 11 | m('tbody', [ 12 | v.attrs.list.map((channel) => 13 | m(util.ChannelSummary, { 14 | details: channel, 15 | category: 'PopularChannels', 16 | }) 17 | ), 18 | v.attrs.list.map((channel) => 19 | m(util.DisplayChannelsFromList, { 20 | id: channel.mGroupId, 21 | category: 'PopularChannels', 22 | }) 23 | ), 24 | ]) 25 | ), 26 | ]), 27 | ], 28 | }; 29 | }; 30 | 31 | module.exports = Layout; 32 | -------------------------------------------------------------------------------- /webui-src/app/channels/subscribed_channels.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('channels/channels_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Subscribed Channels')), 8 | m('.widget__body', [ 9 | m( 10 | util.ChannelTable, 11 | m('tbody', [ 12 | v.attrs.list.map((channel) => 13 | m(util.ChannelSummary, { 14 | details: channel, 15 | category: 'SubscribedChannels', 16 | }) 17 | ), 18 | v.attrs.list.map((channel) => 19 | m(util.DisplayChannelsFromList, { 20 | id: channel.mGroupId, 21 | category: 'SubscribedChannels', 22 | }) 23 | ), 24 | ]) 25 | ), 26 | ]), 27 | ], 28 | }; 29 | }; 30 | 31 | module.exports = Layout; 32 | -------------------------------------------------------------------------------- /webui-src/app/config/config_files.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | const util = require('config/config_util'); 4 | 5 | const SharedDirectories = () => { 6 | let directories = []; 7 | return { 8 | oninit: () => { 9 | rs.rsJsonApiRequest('/rsFiles/getSharedDirectories', {}, (data) => (directories = data.dirs)); 10 | }, 11 | view: () => 12 | m('.widget__body-box', [ 13 | m('.widget__heading', m('h3', 'Shared Directories')), 14 | directories.map((dir) => 15 | m('input[type=text].stretched', { 16 | value: dir.filename, 17 | }) 18 | ), 19 | ]), 20 | }; 21 | }; 22 | 23 | const DownloadDirectory = () => { 24 | let dlDir = ''; 25 | const setDir = () => { 26 | rs.rsJsonApiRequest('rsFiles/setDownloadDirectory', { 27 | path: dlDir, 28 | }); 29 | }; 30 | return { 31 | oninit: () => { 32 | rs.rsJsonApiRequest('/rsFiles/getDownloadDirectory', {}, (data) => (dlDir = data.retval)); 33 | }, 34 | view: () => 35 | m('.widget__body-box', [ 36 | m('.widget__heading', m('h3', 'Downloads Directory')), 37 | m('input[type=text].stretched#dl-dir-input', { 38 | oninput: (e) => (dlDir = e.target.value), 39 | value: dlDir, 40 | onchange: setDir, 41 | }), 42 | ]), 43 | }; 44 | }; 45 | 46 | const PartialsDirectory = () => { 47 | let partialsDir = ''; 48 | const setDir = () => { 49 | // const path = document.getElementById('partial-dir-input').value; // unused? 50 | 51 | rs.rsJsonApiRequest('rsFiles/setPartialsDirectory', { 52 | path: partialsDir, 53 | }); 54 | }; 55 | return { 56 | oninit: () => 57 | rs.rsJsonApiRequest( 58 | '/rsFiles/getPartialsDirectory', 59 | {}, 60 | (data) => (partialsDir = data.retval) 61 | ), 62 | view: () => 63 | m('.widget__body-box', [ 64 | m('.widget__heading', m('h3', 'Partials Directory')), 65 | m('input[type=text].stretched#partial-dir-input', { 66 | oninput: (e) => (partialsDir = e.target.value), 67 | value: partialsDir, 68 | onchange: setDir, 69 | }), 70 | ]), 71 | }; 72 | }; 73 | 74 | const TransferOptions = () => { 75 | let queueSize = undefined; 76 | let maxUploadSlots = undefined; 77 | let strategy = undefined; 78 | let diskLimit = undefined; 79 | let encryptionPolicy = undefined; 80 | let directDLPerm = undefined; 81 | const setMaxSimultaneousDownloads = () => 82 | rs.rsJsonApiRequest('/rsFiles/setQueueSize', { 83 | s: parseInt(queueSize), 84 | }); 85 | const setMaxUploadSlots = () => 86 | rs.rsJsonApiRequest('/rsFiles/setMaxUploadSlotsPerFriend', { 87 | n: parseInt(maxUploadSlots), 88 | }); 89 | const setChunkStrat = () => 90 | rs.rsJsonApiRequest('/rsFiles/setDefaultChunkStrategy', { 91 | strategy: parseInt(strategy), 92 | }); 93 | const setFreeLimit = () => 94 | rs.rsJsonApiRequest('/rsFiles/setFreeDiskSpaceLimit', { 95 | minimumFreeMB: parseInt(diskLimit), 96 | }); 97 | const setDefaultEncryption = () => { 98 | rs.rsJsonApiRequest('/rsFiles/setDefaultEncryptionPolicy', { 99 | policy: parseInt(encryptionPolicy), 100 | }); 101 | }; 102 | const setDirectDLPerm = () => { 103 | rs.rsJsonApiRequest('/rsFiles/setFilePermDirectDL', { 104 | perm: parseInt(directDLPerm), 105 | }); 106 | }; 107 | return { 108 | oninit: () => { 109 | rs.rsJsonApiRequest('/rsFiles/getQueueSize').then((res) => (queueSize = res.body.retval)); 110 | rs.rsJsonApiRequest('/rsFiles/defaultChunkStrategy', {}, (data) => (strategy = data.retval)); 111 | rs.rsJsonApiRequest('/rsFiles/getMaxUploadSlotsPerFriend').then( 112 | (res) => (maxUploadSlots = res.body.retval) 113 | ); 114 | rs.rsJsonApiRequest('/rsFiles/freeDiskSpaceLimit', {}, (data) => (diskLimit = data.retval)); 115 | rs.rsJsonApiRequest('/rsFiles/defaultEncryptionPolicy').then( 116 | (res) => (encryptionPolicy = res.body.retval) 117 | ); 118 | rs.rsJsonApiRequest('/rsFiles/filePermDirectDL').then( 119 | (res) => (directDLPerm = res.body.retval) 120 | ); 121 | }, 122 | view: () => 123 | m('.widget__body-box', [ 124 | m('.widget__heading', m('h3', 'Transfer options')), 125 | m('.grid-2col', [ 126 | m('p', 'Maximum simultaneous downloads:'), 127 | m('input[type=number]', { 128 | value: queueSize, 129 | oninput: (e) => (queueSize = e.target.value), 130 | onchange: setMaxSimultaneousDownloads, 131 | }), 132 | m('p', 'Default chunk strategy:'), 133 | m( 134 | 'select[name=strategy]', 135 | { 136 | value: strategy, 137 | oninput: (e) => (strategy = e.target.value), 138 | onchange: setChunkStrat, 139 | }, 140 | ['Streaming', 'Random', 'Progressive'].map((val, i) => 141 | m('option[value=' + i + ']', val) 142 | ) 143 | ), 144 | m('p', 'Maximum uploads per friend:'), 145 | m('input[type=number]', { 146 | value: maxUploadSlots, 147 | oninput: (e) => (maxUploadSlots = e.target.value), 148 | onchange: setMaxUploadSlots, 149 | }), 150 | m('p', 'Safety disk space limit(MB):'), 151 | m('input[type=number]', { 152 | value: diskLimit, 153 | oninput: (e) => (diskLimit = e.target.value), 154 | onchange: setFreeLimit, 155 | }), 156 | m('p', 'End-to-end encryption:'), 157 | m( 158 | 'select', 159 | { 160 | value: encryptionPolicy, 161 | oninput: (e) => (encryptionPolicy = e.target.value), 162 | onchange: setDefaultEncryption, 163 | }, 164 | [ 165 | m( 166 | 'option', 167 | { 168 | value: util.RS_FILE_CTRL_ENCRYPTION_POLICY_STRICT, 169 | }, 170 | 'Enforced' 171 | ), 172 | m( 173 | 'option', 174 | { 175 | value: util.RS_FILE_CTRL_ENCRYPTION_POLICY_PERMISSIVE, 176 | }, 177 | 'Accepted' 178 | ), 179 | ] 180 | ), 181 | m('p', 'Allow Direct Download:'), 182 | m( 183 | 'select', 184 | { 185 | value: directDLPerm, 186 | oninput: (e) => (directDLPerm = e.target.value), 187 | onchange: setDirectDLPerm, 188 | }, 189 | [ 190 | m( 191 | 'option', 192 | { 193 | value: util.RS_FILE_PERM_DIRECT_DL_YES, 194 | }, 195 | 'Yes' 196 | ), 197 | m( 198 | 'option', 199 | { 200 | value: util.RS_FILE_PERM_DIRECT_DL_NO, 201 | }, 202 | 'No' 203 | ), 204 | m( 205 | 'option', 206 | { 207 | value: util.RS_FILE_PERM_DIRECT_DL_PER_USER, 208 | }, 209 | 'Per User' 210 | ), 211 | ] 212 | ), 213 | ]), 214 | ]), 215 | }; 216 | }; 217 | 218 | const Layout = () => { 219 | return { 220 | view: () => 221 | m('.widget', [ 222 | m('.widget__heading', m('h3', 'Files Configuration')), 223 | m('.widget__body.config-files', [ 224 | m(SharedDirectories), 225 | m(DownloadDirectory), 226 | m(PartialsDirectory), 227 | m(TransferOptions), 228 | ]), 229 | ]), 230 | }; 231 | }; 232 | 233 | module.exports = Layout; -------------------------------------------------------------------------------- /webui-src/app/config/config_mail.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | const widget = require('widgets'); 4 | const util = require('config/config_util'); 5 | 6 | const msgTagObj = { 7 | tagId: 100, 8 | tagName: '', 9 | tagColor: '', 10 | }; 11 | let tagArr = []; 12 | 13 | async function handleSubmit(tagId) { 14 | const modalContainer = document.getElementById('modal-container'); 15 | msgTagObj.tagId = typeof tagId === 'number' ? tagId : util.getRandomId(tagArr); 16 | let tagNameAlreadyExists = false; 17 | tagArr.forEach((item) => { 18 | if (item.value.first === msgTagObj.tagName) tagNameAlreadyExists = true; 19 | }); 20 | if (tagNameAlreadyExists) { 21 | alert('Tag Name Already Exists'); 22 | } else { 23 | rs.rsJsonApiRequest('/rsMsgs/setMessageTagType', { 24 | tagId: msgTagObj.tagId, 25 | text: msgTagObj.tagName, 26 | rgb_color: parseInt(msgTagObj.tagColor.substring(1), 16), 27 | }); 28 | modalContainer.style.display = 'none'; 29 | rs.rsJsonApiRequest('/rsMsgs/getMessageTagTypes').then((res) => (tagArr = res.body.tags.types)); 30 | } 31 | } 32 | 33 | const MessageTagForm = () => { 34 | return { 35 | view: (v) => { 36 | const isCreateForm = v.attrs.tagItem === undefined; 37 | return m( 38 | 'form.mail-tags-form', 39 | { 40 | onsubmit: isCreateForm ? handleSubmit : () => handleSubmit(v.attrs.tagItem.key), 41 | }, 42 | [ 43 | m('h3', isCreateForm ? 'Create New Tag Type' : 'Edit Tag Type'), 44 | m('hr'), 45 | m('.input-field', [ 46 | m('label[for=tagName]', 'Enter Tag Name'), 47 | m('input[type=text][id=tagName][placeholder="enter tag name"]', { 48 | value: msgTagObj.tagName, 49 | oninput: (e) => (msgTagObj.tagName = e.target.value), 50 | }), 51 | ]), 52 | m('.input-field', [ 53 | m('label[for=tagColor]', 'Choose Tag Color'), 54 | m('input[type=color][id=tagColor]', { 55 | value: msgTagObj.tagColor, 56 | oninput: (e) => (msgTagObj.tagColor = e.target.value), 57 | }), 58 | ]), 59 | // v.attrs.tagItem !== undefined && m('p', v.attrs.tagItem.value.first), 60 | m('button[type=submit]', 'Submit'), 61 | ] 62 | ); 63 | }, 64 | }; 65 | }; 66 | 67 | const Mail = () => { 68 | let distantMessagingPermissionFlag = 0; 69 | return { 70 | oninit: () => { 71 | rs.rsJsonApiRequest('/rsMsgs/getMessageTagTypes').then( 72 | (res) => (tagArr = res.body.tags.types) 73 | ); 74 | rs.rsJsonApiRequest('/rsMsgs/getDistantMessagingPermissionFlags').then( 75 | (res) => (distantMessagingPermissionFlag = res.body.retval) 76 | ); 77 | }, 78 | view: () => 79 | m('.widget.mail', [ 80 | m('.widget__heading', m('h3', 'Mail Configuration')), 81 | m('.widget__body', [ 82 | m('.permission-flag', [ 83 | m('p', 'Accept encrypted distant messages from: '), 84 | m( 85 | 'select', 86 | { 87 | value: distantMessagingPermissionFlag, 88 | oninput: (e) => (distantMessagingPermissionFlag = e.target.value), 89 | onchange: () => { 90 | rs.rsJsonApiRequest('/rsMsgs/setDistantMessagingPermissionFlags', { 91 | flags: parseInt(distantMessagingPermissionFlag), 92 | }); 93 | }, 94 | }, 95 | [ 96 | m( 97 | 'option', 98 | { 99 | value: util.RS_DISTANT_MESSAGING_PERMISSION_FLAG_FILTER_NONE, 100 | }, 101 | 'Everybody' 102 | ), 103 | m( 104 | 'option', 105 | { 106 | value: util.RS_DISTANT_MESSAGING_PERMISSION_FLAG_FILTER_NON_CONTACTS, 107 | }, 108 | 'Contacts' 109 | ), 110 | m( 111 | 'option', 112 | { 113 | value: util.RS_DISTANT_MESSAGING_PERMISSION_FLAG_FILTER_EVERYBODY, 114 | }, 115 | 'Nobody' 116 | ), 117 | ] 118 | ), 119 | ]), 120 | m('.widget__heading', [ 121 | m('h3', 'Mail Tags'), 122 | m( 123 | 'button', 124 | { 125 | onclick: () => { 126 | // set form fields to default values 127 | msgTagObj.tagName = ''; 128 | msgTagObj.tagColor = ''; 129 | widget.popupMessage(m(MessageTagForm)); 130 | }, 131 | }, 132 | 'Create New Tag' 133 | ), 134 | ]), 135 | m( 136 | '.mail-tags', 137 | tagArr.length === 0 138 | ? m('h4', 'No Message Tags') 139 | : m( 140 | '.mail-tags__container', 141 | tagArr.map((tag) => 142 | m('.tag-item', { key: tag.key }, [ 143 | m('.tag-item__color', { 144 | style: { 145 | backgroundColor: `#${tag.value.second.toString(16).padStart(6, '0')}`, 146 | }, 147 | }), 148 | m('p.tag-item__name', tag.value.first), 149 | m('.tag-item__modify', [ 150 | m( 151 | 'button', 152 | { 153 | onclick: () => { 154 | msgTagObj.tagName = tag.value.first; 155 | msgTagObj.tagColor = `#${tag.value.second 156 | .toString(16) 157 | .padStart(6, '0')}`; 158 | widget.popupMessage(m(MessageTagForm, { tagItem: tag })); 159 | }, 160 | }, 161 | m('i.fas.fa-pen') 162 | ), 163 | m( 164 | 'button.red', 165 | { 166 | onclick: () => { 167 | rs.rsJsonApiRequest('/rsMsgs/removeMessageTagType', { 168 | tagId: tag.key, 169 | }).then((res) => { 170 | if (res.body.retval) 171 | tagArr = tagArr.filter((item) => item.key !== tag.key); 172 | }); 173 | }, 174 | }, 175 | m('i.fas.fa-trash') 176 | ), 177 | ]), 178 | ]) 179 | ) 180 | ) 181 | ), 182 | ]), 183 | ]), 184 | }; 185 | }; 186 | 187 | module.exports = Mail; 188 | -------------------------------------------------------------------------------- /webui-src/app/config/config_node.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | 4 | const Node = () => { 5 | const nodeInfo = { 6 | setData(data) { 7 | Object.assign(nodeInfo, data.status); 8 | }, 9 | }; 10 | return { 11 | oninit() { 12 | rs.rsJsonApiRequest('/rsConfig/getConfigNetStatus', {}, nodeInfo.setData); 13 | }, 14 | view() { 15 | return [ 16 | m('.widget', [ 17 | m('.widget__heading', m('h3', 'Public Information')), 18 | m('.widget__body', [ 19 | m('ul', [ 20 | m('li', 'Name: ' + nodeInfo.ownName), 21 | m('li', 'Location ID: ' + nodeInfo.ownId), 22 | m('li', 'Firewall: ' + nodeInfo.firewalled), 23 | m('li', 'Port Forwarding: ' + nodeInfo.forwardPort), 24 | m('li', 'DHT: ' + nodeInfo.DHTActive), 25 | m('li', 'uPnP: ' + nodeInfo.uPnPActive), 26 | m('li', 'Local Address: ' + nodeInfo.localAddr + ' Port: ' + nodeInfo.localPort), 27 | ]), 28 | ]), 29 | ]), 30 | ]; 31 | }, 32 | }; 33 | }; 34 | 35 | module.exports = Node; 36 | -------------------------------------------------------------------------------- /webui-src/app/config/config_people.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | 4 | const Reputation = () => { 5 | let addFriendIdAsContacts = undefined; 6 | let usePositiveDefault = undefined; 7 | let deleteBannedAfter = undefined; 8 | let negativeThreshold = undefined; 9 | let positiveThreshold = undefined; 10 | 11 | return { 12 | oninit: (vnode) => { 13 | rs.rsJsonApiRequest( 14 | '/rsIdentity/autoAddFriendIdsAsContact', 15 | {}, 16 | (data) => (addFriendIdAsContacts = data.retval) 17 | ); 18 | rs.rsJsonApiRequest( 19 | '/rsreputations/autoPositiveOpinionForContacts', 20 | {}, 21 | (data) => (usePositiveDefault = data.retval) 22 | ); 23 | rs.rsJsonApiRequest( 24 | '/rsIdentity/deleteBannedNodesThreshold', 25 | {}, 26 | (data) => (deleteBannedAfter = data.retval) 27 | ); 28 | rs.rsJsonApiRequest( 29 | '/rsreputations/thresholdForRemotelyPositiveReputation', 30 | {}, 31 | (data) => (positiveThreshold = data.retval) 32 | ); 33 | rs.rsJsonApiRequest( 34 | '/rsreputations/thresholdForRemotelyNegativeReputation', 35 | {}, 36 | (data) => (negativeThreshold = data.retval) 37 | ); 38 | }, 39 | view: (vnode) => 40 | m('.widget', [ 41 | m('.widget__heading', m('h3', 'Reputation')), 42 | m('.widget__body', [ 43 | m('.grid-2col', [ 44 | m('p', 'Use "positive" as the default opinion for contacts(instead of neutral):'), 45 | m('input[type=checkbox]', { 46 | checked: usePositiveDefault, 47 | oninput: (e) => { 48 | usePositiveDefault = e.target.checked; 49 | rs.rsJsonApiRequest( 50 | '/rsreputations/setAutoPositiveOpinionForContacts', 51 | { 52 | b: usePositiveDefault, 53 | }, 54 | () => {} 55 | ); 56 | }, 57 | }), 58 | m('p', 'Automatically add identities owned by friend nodes to my contacts:'), 59 | m('input[type=checkbox]', { 60 | checked: addFriendIdAsContacts, 61 | oninput: (e) => { 62 | addFriendIdAsContacts = e.target.checked; 63 | rs.rsJsonApiRequest( 64 | '/rsIdentity/setAutoAddFriendIdsAsContact', 65 | { 66 | enabled: addFriendIdAsContacts, 67 | }, 68 | () => {} 69 | ); 70 | }, 71 | }), 72 | m('p', 'Difference in votes (+/-) to rate an ID positively:'), 73 | m('input[type=number]', { 74 | oninput: (e) => (positiveThreshold = e.target.value), 75 | value: positiveThreshold, 76 | onchange: () => 77 | rs.rsJsonApiRequest( 78 | '/rsreputations/setThresholdForRemotelyPositiveReputation', 79 | { 80 | thresh: positiveThreshold, 81 | }, 82 | () => {} 83 | ), 84 | }), 85 | m('p', 'Difference in votes (+/-) to rate an ID negatively:'), 86 | m('input[type=number]', { 87 | oninput: (e) => (negativeThreshold = e.target.value), 88 | value: negativeThreshold, 89 | onchange: () => 90 | rs.rsJsonApiRequest( 91 | '/rsreputations/setThresholdForRemotelyNegativeReputation', 92 | { 93 | thresh: negativeThreshold, 94 | }, 95 | () => {} 96 | ), 97 | }), 98 | m('p', 'Delete banned identities after(in days, 0 means indefinitely):'), 99 | m('input[type=number]', { 100 | oninput: (e) => (deleteBannedAfter = e.target.value), 101 | value: deleteBannedAfter, 102 | onchange: () => 103 | rs.rsJsonApiRequest( 104 | '/rsIdentity/setDeleteBannedNodesThreshold', 105 | { 106 | days: deleteBannedAfter, 107 | }, 108 | () => {} 109 | ), 110 | }), 111 | ]), 112 | ]), 113 | ]), 114 | }; 115 | }; 116 | 117 | const Layout = () => { 118 | return { 119 | view: (vnode) => [m(Reputation)], 120 | }; 121 | }; 122 | 123 | module.exports = Layout; 124 | -------------------------------------------------------------------------------- /webui-src/app/config/config_resolver.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const widget = require('widgets'); 3 | 4 | const sections = { 5 | network: require('config/config_network'), 6 | node: require('config/config_node'), 7 | services: require('config/config_services'), 8 | files: require('config/config_files'), 9 | people: require('config/config_people'), 10 | mail: require('config/config_mail'), 11 | }; 12 | 13 | const Layout = { 14 | view: (vnode) => [ 15 | m(widget.Sidebar, { 16 | tabs: Object.keys(sections), 17 | baseRoute: '/config/', 18 | }), 19 | m('.node-panel', vnode.children), 20 | ], 21 | }; 22 | 23 | module.exports = { 24 | view: (vnode) => { 25 | const tab = vnode.attrs.tab; 26 | return m(Layout, m(sections[tab])); 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /webui-src/app/config/config_services.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | 4 | const servicesInfo = { 5 | list: [], 6 | 7 | setData(data) { 8 | servicesInfo.list = data.info.mServiceList; 9 | }, 10 | }; 11 | 12 | const Service = () => { 13 | let defaultAllowed = undefined; 14 | return { 15 | oninit: (v) => 16 | rs.rsJsonApiRequest( 17 | '/rsServiceControl/getServicePermissions', 18 | { 19 | serviceId: v.attrs.data.key, 20 | }, 21 | (retval) => (defaultAllowed = retval.permissions.mDefaultAllowed) 22 | ), 23 | view: (v) => 24 | m( 25 | 'tr', 26 | { 27 | key: v.attrs.data.key, 28 | }, 29 | [ 30 | m('td', v.attrs.data.value.mServiceName), 31 | m('td', v.attrs.data.value.mServiceType), 32 | m('td', v.attrs.data.value.mVersionMajor + '.' + v.attrs.data.value.mVersionMinor), 33 | m( 34 | 'td', 35 | m('input[type=checkbox]', { 36 | checked: defaultAllowed, 37 | oninput: (e) => { 38 | defaultAllowed = e.target.checked; 39 | rs.rsJsonApiRequest('/rsServiceControl/updateServicePermissions', { 40 | serviceId: v.attrs.data.key, 41 | permissions: { 42 | mDefaultAllowed: defaultAllowed, 43 | }, 44 | }); 45 | }, 46 | }) 47 | ), 48 | ] 49 | ), 50 | }; 51 | }; 52 | 53 | const MyServices = { 54 | oninit() { 55 | rs.rsJsonApiRequest('/rsServiceControl/getOwnServices', {}, servicesInfo.setData); 56 | }, 57 | view() { 58 | return m('.widget', [ 59 | m('.widget__heading', m('h3', 'My Services')), 60 | m('.widget__body', [ 61 | m('table', [ 62 | m('tr', [ 63 | m('th', 'Name'), 64 | m('th', 'ID'), 65 | m('th', 'Version'), 66 | m('th', 'Allow by default'), 67 | ]), 68 | servicesInfo.list.map((data) => 69 | m(Service, { 70 | data, 71 | }) 72 | ), 73 | ]), 74 | ]), 75 | ]); 76 | }, 77 | }; 78 | 79 | module.exports = { 80 | view: () => { 81 | return m(MyServices); 82 | }, 83 | }; 84 | -------------------------------------------------------------------------------- /webui-src/app/config/config_util.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | 3 | /* Visibility parameter for discovery */ 4 | const RS_VS_DISC_OFF = 0x0000; 5 | const RS_VS_DISC_MINIMAL = 0x0001; 6 | const RS_VS_DISC_FULL = 0x0002; 7 | 8 | const RS_VS_DHT_OFF = 0x0000; 9 | const RS_VS_DHT_PASSIVE = 0x0001; 10 | const RS_VS_DHT_FULL = 0x0002; 11 | 12 | const MAX_TAG_ID_VAL = 1000000; 13 | const MIN_TAG_ID_VAL = 100; 14 | 15 | // Distant Messaging Permission Flags to define who we accept to talk to. 16 | // Each flag *removes* some people. 17 | const RS_DISTANT_MESSAGING_PERMISSION_FLAG_FILTER_NONE = 0; 18 | const RS_DISTANT_MESSAGING_PERMISSION_FLAG_FILTER_NON_CONTACTS = 1; 19 | const RS_DISTANT_MESSAGING_PERMISSION_FLAG_FILTER_EVERYBODY = 2; 20 | 21 | // Hidden Service Configuration Type 22 | const RS_HIDDEN_TYPE_NONE = 0; 23 | const RS_HIDDEN_TYPE_UNKNOWN = 1; 24 | const RS_HIDDEN_TYPE_TOR = 2; 25 | const RS_HIDDEN_TYPE_I2P = 4; 26 | 27 | // NAT Net Mode 28 | const RS_NETMODE_UDP = 1; 29 | const RS_NETMODE_UPNP = 2; 30 | const RS_NETMODE_EXT = 3; 31 | 32 | // Default Encryption Policy 33 | const RS_FILE_CTRL_ENCRYPTION_POLICY_STRICT = 1; 34 | const RS_FILE_CTRL_ENCRYPTION_POLICY_PERMISSIVE = 2; 35 | 36 | // Direct Download Permission 37 | const RS_FILE_PERM_DIRECT_DL_YES = 1; 38 | const RS_FILE_PERM_DIRECT_DL_NO = 2; 39 | const RS_FILE_PERM_DIRECT_DL_PER_USER = 3; 40 | 41 | function getRandomId(tagArr) { 42 | const random = Math.floor(Math.random() * (MAX_TAG_ID_VAL - MIN_TAG_ID_VAL) + MIN_TAG_ID_VAL); 43 | tagArr.forEach((tag) => { 44 | if (tag.key === random) { 45 | return getRandomId(tagArr); 46 | } 47 | }); 48 | return random; 49 | } 50 | 51 | function tooltip(text) { 52 | return m('.tooltip', [m('i.fas.fa-info-circle'), m('.tooltiptext', text)]); 53 | } 54 | 55 | module.exports = { 56 | getRandomId, 57 | tooltip, 58 | RS_VS_DHT_FULL, 59 | RS_VS_DHT_OFF, 60 | RS_VS_DISC_FULL, 61 | RS_VS_DHT_PASSIVE, 62 | RS_VS_DISC_OFF, 63 | RS_VS_DISC_MINIMAL, 64 | RS_DISTANT_MESSAGING_PERMISSION_FLAG_FILTER_NONE, 65 | RS_DISTANT_MESSAGING_PERMISSION_FLAG_FILTER_NON_CONTACTS, 66 | RS_DISTANT_MESSAGING_PERMISSION_FLAG_FILTER_EVERYBODY, 67 | RS_HIDDEN_TYPE_NONE, 68 | RS_HIDDEN_TYPE_UNKNOWN, 69 | RS_HIDDEN_TYPE_TOR, 70 | RS_HIDDEN_TYPE_I2P, 71 | RS_NETMODE_UDP, 72 | RS_NETMODE_UPNP, 73 | RS_NETMODE_EXT, 74 | RS_FILE_CTRL_ENCRYPTION_POLICY_STRICT, 75 | RS_FILE_CTRL_ENCRYPTION_POLICY_PERMISSIVE, 76 | RS_FILE_PERM_DIRECT_DL_YES, 77 | RS_FILE_PERM_DIRECT_DL_NO, 78 | RS_FILE_PERM_DIRECT_DL_PER_USER, 79 | }; 80 | -------------------------------------------------------------------------------- /webui-src/app/files/files_downloads.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | const util = require('files/files_util'); 4 | const widget = require('widgets'); 5 | 6 | const Downloads = { 7 | strategies: {}, 8 | statusMap: {}, 9 | hashes: [], 10 | chunksMap: {}, 11 | 12 | loadStrategy() { 13 | rs.rsJsonApiRequest('/rsFiles/FileDownloads', {}, (d) => 14 | d.hashs.map((hash) => { 15 | rs.rsJsonApiRequest('/rsFiles/getChunkStrategy', { hash }).then((res) => { 16 | if (res.body.retval) Downloads.strategies[hash] = res.body.s; 17 | }); 18 | }) 19 | ); 20 | }, 21 | 22 | async loadHashes() { 23 | await rs 24 | .rsJsonApiRequest('/rsFiles/FileDownloads', {}, (d) => (Downloads.hashes = d.hashs)) 25 | .then(() => { 26 | Downloads.hashes.forEach((hash) => { 27 | rs.rsJsonApiRequest('/rsFiles/FileDownloadChunksDetails', { 28 | hash, 29 | }).then((res) => (this.chunksMap[hash] = res.body.info)); 30 | }); 31 | }); 32 | }, 33 | 34 | async loadStatus() { 35 | await Downloads.loadHashes(); 36 | const fileKeys = Object.keys(Downloads.statusMap); 37 | if (Downloads.hashes !== undefined && Downloads.hashes.length !== fileKeys.length) { 38 | if (Downloads.hashes.length > fileKeys.length) { 39 | // New file added 40 | const newHashes = util.compareArrays(Downloads.hashes, fileKeys); 41 | for (const hash of newHashes) { 42 | Downloads.updateFileDetail(hash, true); 43 | } 44 | } else { 45 | // Existing file removed 46 | const oldHashes = util.compareArrays(fileKeys, Downloads.hashes); 47 | for (const hash of oldHashes) { 48 | delete Downloads.statusMap[hash]; 49 | } 50 | } 51 | } 52 | for (const hash in Downloads.statusMap) { 53 | Downloads.updateFileDetail(hash); 54 | } 55 | }, 56 | resetSearch() { 57 | for (const hash in Downloads.statusMap) { 58 | Downloads.statusMap[hash].isSearched = true; 59 | } 60 | }, 61 | updateFileDetail(hash, isNew = false) { 62 | rs.rsJsonApiRequest( 63 | '/rsFiles/FileDetails', 64 | { 65 | hash, 66 | hintflags: 16, // RS_FILE_HINTS_DOWNLOAD 67 | }, 68 | (fileStat) => { 69 | if (!fileStat.retval) { 70 | console.error('Error: Unknown hash in Downloads: ', hash); 71 | return; 72 | } 73 | fileStat.info.isSearched = isNew ? true : Downloads.statusMap[hash].isSearched; 74 | Downloads.statusMap[hash] = fileStat.info; 75 | } 76 | ); 77 | }, 78 | }; 79 | 80 | function InvalidFileMessage() { 81 | widget.popupMessage([ 82 | m('i.fas.fa-file-medical'), 83 | m('h3', 'Add new file'), 84 | m('hr'), 85 | m('p', 'Error: could not add file'), 86 | ]); 87 | } 88 | 89 | function addFile(url) { 90 | // valid url format: retroshare://file?name=...&size=...&hash=... 91 | if (!url.startsWith('retroshare://')) { 92 | InvalidFileMessage(); 93 | return; 94 | } 95 | const details = m.parseQueryString(url.split('?')[1]); 96 | if ( 97 | !Object.prototype.hasOwnProperty.call(details, 'name') || 98 | !Object.prototype.hasOwnProperty.call(details, 'size') || 99 | !Object.prototype.hasOwnProperty.call(details, 'hash') 100 | ) { 101 | InvalidFileMessage(); 102 | return; 103 | } 104 | rs.rsJsonApiRequest( 105 | '/rsFiles/FileRequest', 106 | { 107 | fileName: details.name, 108 | hash: details.hash, 109 | flags: util.RS_FILE_REQ_ANONYMOUS_ROUTING, 110 | size: { 111 | xstr64: details.size, 112 | }, 113 | }, 114 | (status) => { 115 | widget.popupMessage([ 116 | m('i.fas.fa-file-medical'), 117 | m('h3', 'Add new file'), 118 | m('hr'), 119 | m('p', 'Successfully added file!'), 120 | ]); 121 | } 122 | ); 123 | } 124 | 125 | const NewFileDialog = () => { 126 | let url = ''; 127 | return { 128 | view: () => [ 129 | m('i.fas.fa-file-medical'), 130 | m('h3', 'Add new file'), 131 | m('hr'), 132 | m('p', 'Enter the file link:'), 133 | m('input[type=text][name=fileurl]', { 134 | onchange: (e) => (url = e.target.value), 135 | }), 136 | m('button', { onclick: () => addFile(url) }, 'Add'), 137 | ], 138 | }; 139 | }; 140 | 141 | const Component = () => { 142 | function clearFileCompleted() { 143 | rs.rsJsonApiRequest('/rsFiles/FileClearCompleted'); 144 | } 145 | return { 146 | oninit: () => { 147 | Downloads.loadStrategy(); 148 | rs.setBackgroundTask(Downloads.loadStatus, 1000, () => m.route.get() === '/files/files'); 149 | Downloads.resetSearch(); 150 | }, 151 | view: () => [ 152 | m('.widget__body-heading', [ 153 | m('h3', `Downloads (${Downloads.hashes ? Downloads.hashes.length : 0} files)`), 154 | m('.action', [ 155 | m('button', { onclick: () => widget.popupMessage(m(NewFileDialog)) }, 'Add new file'), 156 | m('button', { onclick: clearFileCompleted }, 'Clear completed'), 157 | ]), 158 | ]), 159 | m('.widget__body-content', [ 160 | Downloads.statusMap && 161 | Object.keys(Downloads.statusMap).map((hash) => 162 | m(util.File, { 163 | info: Downloads.statusMap[hash], 164 | strategy: Downloads.strategies[hash], 165 | direction: 'down', 166 | transferred: Downloads.statusMap[hash].transfered.xint64, 167 | chunksInfo: Downloads.chunksMap[hash], 168 | }) 169 | ), 170 | ]), 171 | ], 172 | }; 173 | }; 174 | 175 | module.exports = { 176 | Component, 177 | Downloads, 178 | list: Downloads.statusMap, 179 | }; 180 | -------------------------------------------------------------------------------- /webui-src/app/files/files_proxy.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | const futil = require('files/files_util'); 4 | 5 | const fileProxyObj = futil.createProxy({}, () => { 6 | m.redraw(); 7 | }); 8 | 9 | rs.events[rs.RsEventsType.FILE_TRANSFER] = { 10 | handler: (event) => { 11 | console.log('search results : ', event); 12 | 13 | // if request item doesn't already exists in Object then create new item 14 | if (!Object.prototype.hasOwnProperty.call(fileProxyObj, event.mRequestId)) { 15 | fileProxyObj[event.mRequestId] = []; 16 | } 17 | 18 | fileProxyObj[event.mRequestId].push(...event.mResults); 19 | }, 20 | }; 21 | 22 | module.exports = { 23 | fileProxyObj, 24 | }; 25 | -------------------------------------------------------------------------------- /webui-src/app/files/files_resolver.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | 3 | const widget = require('widgets'); 4 | 5 | const downloads = require('files/files_downloads'); 6 | const uploads = require('files/files_uploads'); 7 | const util = require('files/files_util'); 8 | const search = require('files/files_search'); 9 | const myfile = require('files/my_files'); 10 | const friendfile = require('files/friends_files'); 11 | 12 | const MyFiles = () => { 13 | return { 14 | view: () => [ 15 | m('.widget__heading', [ 16 | m('h3', 'File Transfers'), 17 | m(util.SearchBar, { 18 | list: Object.assign({}, downloads.list, uploads.list), 19 | }), 20 | ]), 21 | m('.widget__body', [m(downloads.Component), m(uploads.Component)]), 22 | ], 23 | }; 24 | }; 25 | 26 | const sections = { 27 | files: MyFiles, 28 | search, 29 | MyFiles: myfile, 30 | FriendsFiles: friendfile, 31 | }; 32 | 33 | const Layout = { 34 | view: (vnode) => [ 35 | m(widget.Sidebar, { 36 | tabs: Object.keys(sections), 37 | baseRoute: '/files/', 38 | }), 39 | m('.node-panel', m('.widget', vnode.children)), 40 | ], 41 | }; 42 | 43 | module.exports = { 44 | view: (vnode) => { 45 | const tab = vnode.attrs.tab; 46 | return m(Layout, m(sections[tab])); 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /webui-src/app/files/files_search.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | const widget = require('widgets'); 4 | const futil = require('files/files_util'); 5 | const fproxy = require('files/files_proxy'); 6 | 7 | let matchString = ''; 8 | let currentItem = 0; 9 | const reqObj = {}; 10 | 11 | function handleSubmit() { 12 | rs.rsJsonApiRequest('/rsFiles/turtleSearch', { matchString }) 13 | .then((res) => { 14 | // Add prefix to obj keys so that javascript doesn't sort them 15 | reqObj['_' + res.body.retval] = matchString; 16 | currentItem = '_' + res.body.retval; 17 | }) 18 | .catch((error) => console.log(error)); 19 | } 20 | 21 | const SearchBar = () => { 22 | return { 23 | view: () => 24 | m('form.search-form', { onsubmit: handleSubmit }, [ 25 | m('input[type=text][placeholder=search keyword]', { 26 | value: matchString, 27 | oninput: (e) => (matchString = e.target.value), 28 | }), 29 | m('button[type=submit]', m('i.fas.fa-search')), 30 | ]), 31 | }; 32 | }; 33 | 34 | const Layout = () => { 35 | let active = 0; 36 | function handleFileDownload(item) { 37 | rs.rsJsonApiRequest('/rsFiles/FileRequest', { 38 | fileName: item.fName, 39 | hash: item.fHash, 40 | flags: futil.RS_FILE_REQ_ANONYMOUS_ROUTING, 41 | size: { 42 | xstr64: item.fSize.xstr64, 43 | }, 44 | }) 45 | .then((res) => { 46 | widget.popupMessage( 47 | m('.widget', [ 48 | m('.widget__heading', m('h3', m('i.fas.fa-file-medical'), ' File Download')), 49 | m( 50 | '.widget__body', 51 | m('p', `File is ${res.body.retval ? 'getting' : 'already'} downloaded.`) 52 | ), 53 | ]) 54 | ); 55 | }) 56 | .catch((error) => { 57 | console.log('error in sending download request: ', error); 58 | }); 59 | } 60 | return { 61 | view: () => [ 62 | m('.widget__heading', [m('h3', 'Search'), m(SearchBar)]), 63 | m('.widget__body', [ 64 | m('div.file-search-container', [ 65 | m('div.file-search-container__keywords', [ 66 | m('h5.bold', 'Keywords'), 67 | Object.keys(reqObj).length !== 0 && 68 | m( 69 | 'div.keywords-container', 70 | Object.keys(reqObj) 71 | .reverse() 72 | .map((item, index) => { 73 | return m( 74 | m.route.Link, 75 | { 76 | class: active === index ? 'selected' : '', 77 | onclick: () => { 78 | active = index; 79 | currentItem = item; 80 | }, 81 | href: `/files/search/${item}`, 82 | }, 83 | reqObj[item] 84 | ); 85 | }) 86 | ), 87 | ]), 88 | m('div.file-search-container__results', [ 89 | Object.keys(fproxy.fileProxyObj).length === 0 90 | ? m('h5.bold', 'Results') 91 | : m('table.results-container', [ 92 | m( 93 | 'thead.results-header', 94 | m('tr', [ 95 | m('th', 'Name'), 96 | m('th', 'Size'), 97 | m('th', 'Hash'), 98 | m('th', 'Download'), 99 | ]) 100 | ), 101 | m( 102 | 'tbody.results', 103 | fproxy.fileProxyObj[currentItem.slice(1)] 104 | ? fproxy.fileProxyObj[currentItem.slice(1)].map((item) => 105 | m('tr', [ 106 | m('td.results__name', [m('i.fas.fa-file'), m('span', item.fName)]), 107 | m('td.results__size', rs.formatBytes(item.fSize.xint64)), 108 | m('td.results__hash', item.fHash), 109 | m( 110 | 'td.results__download', 111 | m('button', { onclick: () => handleFileDownload(item) }, 'Download') 112 | ), 113 | ]) 114 | ) 115 | : 'No Results.' 116 | ), 117 | ]), 118 | ]), 119 | ]), 120 | ]), 121 | ], 122 | }; 123 | }; 124 | 125 | module.exports = { 126 | view: () => m(Layout), 127 | }; 128 | -------------------------------------------------------------------------------- /webui-src/app/files/files_uploads.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | const util = require('files/files_util'); 4 | 5 | const Uploads = { 6 | statusMap: {}, 7 | hashes: [], 8 | 9 | loadHashes() { 10 | rs.rsJsonApiRequest('/rsFiles/FileUploads', {}, (d) => (Uploads.hashes = d.hashs)); 11 | }, 12 | 13 | loadStatus() { 14 | Uploads.loadHashes(); 15 | const fileKeys = Object.keys(Uploads.statusMap); 16 | if (Uploads.hashes.length !== fileKeys.length) { 17 | // New file added 18 | if (Uploads.hashes.length > fileKeys.length) { 19 | const newHashes = util.compareArrays(Uploads.hashes, fileKeys); 20 | for (const hash of newHashes) { 21 | Uploads.updateFileDetail(hash, true); 22 | } 23 | } 24 | // Existing file removed 25 | else { 26 | const oldHashes = util.compareArrays(fileKeys, Uploads.hashes); 27 | for (const hash of oldHashes) { 28 | delete Uploads.statusMap[hash]; 29 | } 30 | } 31 | } 32 | for (const hash in Uploads.statusMap) { 33 | Uploads.updateFileDetail(hash); 34 | } 35 | }, 36 | updateFileDetail(hash, isNew = false) { 37 | rs.rsJsonApiRequest( 38 | '/rsFiles/FileDetails', 39 | { 40 | hash, 41 | hintflags: 32, // RS_FILE_HINTS_UPLOAD 42 | }, 43 | (fileStat) => { 44 | if (!fileStat.retval) { 45 | console.error('Error: Unknown hash in Uploads: ', hash); 46 | return; 47 | } 48 | fileStat.info.isSearched = isNew ? true : Uploads.statusMap[hash].isSearched; 49 | Uploads.statusMap[hash] = fileStat.info; 50 | } 51 | ); 52 | }, 53 | }; 54 | 55 | function averageOf(peers) { 56 | return peers.reduce((s, e) => s + e.transfered.xint64, 0) / peers.length; 57 | } 58 | 59 | const Component = () => { 60 | return { 61 | oninit: () => 62 | rs.setBackgroundTask(Uploads.loadStatus, 1000, () => { 63 | return m.route.get() === '/files/files'; 64 | }), 65 | view: () => 66 | Uploads.hashes.length > 0 67 | ? m('.widget', [ 68 | m('h3', 'Uploads (' + Uploads.hashes.length + ' files)'), 69 | m('hr'), 70 | Object.keys(Uploads.statusMap).map((hash) => 71 | m(util.File, { 72 | info: Uploads.statusMap[hash], 73 | direction: 'up', 74 | transferred: averageOf(Uploads.statusMap[hash].peers), 75 | parts: Uploads.statusMap[hash].peers.reduce( 76 | (a, e) => [...a, e.transfered.xint64], 77 | [] 78 | ), 79 | }) 80 | ), 81 | ]) 82 | : [], 83 | }; 84 | }; 85 | 86 | module.exports = { 87 | Component, 88 | list: Uploads.statusMap, 89 | }; 90 | -------------------------------------------------------------------------------- /webui-src/app/files/friends_files.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | const util = require('files/files_util'); 4 | const widget = require('widgets'); 5 | const fileDown = require('files/files_downloads'); 6 | 7 | function displayfiles() { 8 | const childrenList = []; // stores children details 9 | let loaded = false; // checks whether we have loaded the children details or not. 10 | let parStruct; // stores current struct(details, showChild) 11 | let isFile = false; 12 | let haveFile = false; 13 | let isId = false; 14 | let nameOfId; 15 | return { 16 | oninit: async (v) => { 17 | if (v.attrs.par_directory) { 18 | parStruct = v.attrs.par_directory; 19 | if (Number(parStruct.details.hash) !== 0) { 20 | isFile = true; 21 | const res = await rs.rsJsonApiRequest('/rsfiles/alreadyHaveFile', { 22 | // checks if the file is already there with the user 23 | hash: parStruct.details.hash, 24 | }); 25 | haveFile = res.body.retval; 26 | } 27 | } 28 | if (v.attrs.replyDepth === 1 && parStruct) { 29 | isId = true; 30 | const res = await rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { 31 | sslId: parStruct.details.name, 32 | }); 33 | if (res.body.retval) { 34 | nameOfId = res.body.det.name; 35 | } 36 | } 37 | }, 38 | view: (v) => [ 39 | m('tr', [ 40 | parStruct && Object.keys(parStruct.details.children).length 41 | ? m( 42 | 'td', 43 | m('i.fas.fa-angle-right', { 44 | class: 'fa-rotate-' + (parStruct.showChild ? '90' : '0'), 45 | style: 'margin-top:12px', 46 | onclick: () => { 47 | if (!loaded) { 48 | // if it is not already retrieved. 49 | parStruct.details.children.map(async (child) => { 50 | const res = await rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { 51 | handle: child.handle.xint64, 52 | flags: util.RS_FILE_HINTS_REMOTE, 53 | }); 54 | childrenList.push(res.body.details); 55 | loaded = true; 56 | }); 57 | } 58 | parStruct.showChild = !parStruct.showChild; 59 | }, 60 | }) 61 | ) 62 | : m('td', ''), 63 | m( 64 | 'td', 65 | { 66 | style: { 67 | position: 'relative', 68 | '--replyDepth': v.attrs.replyDepth, 69 | left: `calc(30px*${v.attrs.replyDepth})`, 70 | }, 71 | }, 72 | isId 73 | ? nameOfId + ' (' + parStruct.details.name.slice(0, 8) + '...)' 74 | : parStruct.details.name 75 | ), 76 | m('td', rs.formatBytes(parStruct.details.size.xint64)), 77 | isFile && 78 | m( 79 | 'td', 80 | // using the file from files_util to display download. 81 | fileDown.list[parStruct.details.hash] 82 | ? m(util.File, { 83 | info: fileDown.list[parStruct.details.hash], 84 | direction: 'down', 85 | transferred: fileDown.list[parStruct.details.hash].transfered.xint64, 86 | parts: [], 87 | }) 88 | : m( 89 | 'button', 90 | { 91 | style: { fontSize: '0.9em' }, 92 | onclick: async () => { 93 | widget.popupMessage([ 94 | m('p', 'Start Download?'), 95 | m( 96 | 'button', 97 | { 98 | onclick: async () => { 99 | if (!haveFile) { 100 | const res = await rs.rsJsonApiRequest('/rsFiles/FileRequest', { 101 | fileName: parStruct.details.name, 102 | hash: parStruct.details.hash, 103 | flags: util.RS_FILE_REQ_ANONYMOUS_ROUTING, 104 | size: { 105 | xstr64: parStruct.details.size.xstr64, 106 | }, 107 | }); 108 | res.body.retval === false 109 | ? widget.popupMessage([ 110 | m('h3', 'Error'), 111 | m('hr'), 112 | m('p', res.body.errorMessage), 113 | ]) 114 | : widget.popupMessage([ 115 | m('h3', 'Success'), 116 | m('hr'), 117 | m('p', 'Download Started'), 118 | ]); 119 | m.redraw(); 120 | } 121 | }, 122 | }, 123 | 'Start Download' 124 | ), 125 | ]); 126 | }, 127 | }, 128 | 129 | haveFile ? 'Open File' : ['Download', m('i.fas.fa-download')] 130 | ) 131 | ), 132 | ]), 133 | parStruct.showChild && // recursive call to show children 134 | childrenList.map((child) => 135 | m(displayfiles, { 136 | par_directory: { details: child, showChild: false }, 137 | replyDepth: v.attrs.replyDepth + 1, 138 | }) 139 | ), 140 | ], 141 | }; 142 | } 143 | 144 | const Layout = () => { 145 | // let root_handle; 146 | let parent; 147 | return { 148 | oninit: () => { 149 | rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { 150 | flags: util.RS_FILE_HINTS_REMOTE, 151 | }).then((res) => (parent = res)); 152 | }, 153 | view: () => [ 154 | m('.widget__heading', [m('h3', 'Friends Files')]), 155 | m('.widget__body', [ 156 | m( 157 | util.FriendsFilesTable, 158 | m( 159 | 'tbody', 160 | parent && // root 161 | m(displayfiles, { 162 | par_directory: { details: parent.body.details, showChild: false }, 163 | replyDepth: 0, 164 | }) 165 | ) 166 | ), 167 | ]), 168 | ], 169 | }; 170 | }; 171 | 172 | module.exports = Layout; 173 | -------------------------------------------------------------------------------- /webui-src/app/files/my_files.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | const util = require('files/files_util'); 4 | const manager = require('files/files_manager'); 5 | 6 | const DisplayFiles = () => { 7 | const childrenList = []; // stores children details 8 | let loaded = false; // checks whether we have loaded the children details or not. 9 | let parStruct; // stores current struct(details, showChild) 10 | return { 11 | oninit: (v) => { 12 | if (v.attrs.par_directory) { 13 | parStruct = v.attrs.par_directory; 14 | } 15 | }, 16 | view: (v) => [ 17 | m('tr', [ 18 | parStruct && Object.keys(parStruct.details.children).length 19 | ? m( 20 | 'td', 21 | m('i.fas.fa-angle-right', { 22 | class: `fa-rotate-${parStruct.showChild ? '90' : '0'}`, 23 | style: 'margin-top: 0.5rem', 24 | onclick: () => { 25 | if (!loaded) { 26 | // if it is not already retrieved 27 | parStruct.details.children.map(async (child) => { 28 | const res = await rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { 29 | handle: child.handle.xint64, 30 | flags: util.RS_FILE_HINTS_LOCAL, 31 | }); 32 | childrenList.push(res.body.details); 33 | loaded = true; 34 | }); 35 | } 36 | parStruct.showChild = !parStruct.showChild; 37 | }, 38 | }) 39 | ) 40 | : m('td', ''), 41 | m( 42 | 'td', 43 | { 44 | style: { 45 | position: 'relative', 46 | '--replyDepth': v.attrs.replyDepth, 47 | left: `calc(1.5rem*${v.attrs.replyDepth})`, 48 | }, 49 | }, 50 | parStruct.details.name 51 | ), 52 | m('td', rs.formatBytes(parStruct.details.size.xint64)), 53 | ]), 54 | parStruct.showChild && 55 | childrenList.map((child) => 56 | m(DisplayFiles, { 57 | // recursive call 58 | par_directory: { details: child, showChild: false }, 59 | replyDepth: v.attrs.replyDepth + 1, 60 | }) 61 | ), 62 | ], 63 | }; 64 | }; 65 | 66 | const Layout = () => { 67 | // let root_handle; 68 | let parent; 69 | let showShareManager = false; 70 | return { 71 | oninit: () => { 72 | rs.rsJsonApiRequest('/rsfiles/requestDirDetails', {}).then((res) => (parent = res)); 73 | }, 74 | view: () => [ 75 | m('.widget__heading', [ 76 | m('h3', 'My Files'), 77 | m('button', { onclick: () => (showShareManager = true) }, 'Configure shared directories'), 78 | ]), 79 | m('.widget__body', [ 80 | m( 81 | util.MyFilesTable, 82 | m( 83 | 'tbody', 84 | parent && 85 | m(DisplayFiles, { 86 | par_directory: { details: parent.body.details, showChild: false }, 87 | replyDepth: 0, 88 | }) 89 | ) 90 | ), 91 | m( 92 | '.shareManagerPopupOverlay#shareManagerPopup', 93 | { style: { display: showShareManager ? 'block' : 'none' } }, 94 | m( 95 | '.shareManagerPopup', 96 | m(manager), 97 | m( 98 | 'button.red.close-btn', 99 | { onclick: () => (showShareManager = false) }, 100 | m('i.fas.fa-times') 101 | ) 102 | ) 103 | ), 104 | ]), 105 | ], 106 | }; 107 | }; 108 | 109 | module.exports = Layout; 110 | -------------------------------------------------------------------------------- /webui-src/app/forums/forums.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const widget = require('widgets'); 3 | const rs = require('rswebui'); 4 | const util = require('forums/forums_util'); 5 | const viewUtil = require('forums/forum_view'); 6 | const peopleUtil = require('people/people_util'); 7 | 8 | const getForums = { 9 | All: [], 10 | PopularForums: [], 11 | SubscribedForums: [], 12 | MyForums: [], 13 | async load() { 14 | const res = await rs.rsJsonApiRequest('/rsgxsforums/getForumsSummaries'); 15 | getForums.All = res.body.forums; 16 | getForums.PopularForums = getForums.All; 17 | getForums.SubscribedForums = getForums.All.filter( 18 | (forum) => 19 | forum.mSubscribeFlags === util.GROUP_SUBSCRIBE_SUBSCRIBED || 20 | forum.mSubscribeFlags === util.GROUP_MY_FORUM 21 | ); 22 | getForums.MyForums = getForums.All.filter( 23 | (forum) => forum.mSubscribeFlags === util.GROUP_MY_FORUM 24 | ); 25 | }, 26 | }; 27 | const sections = { 28 | MyForums: require('forums/my_forums'), 29 | SubscribedForums: require('forums/subscribed_forums'), 30 | PopularForums: require('forums/popular_forums'), 31 | OtherForums: require('forums/other_forums'), 32 | }; 33 | 34 | const Layout = () => { 35 | let ownId; 36 | 37 | return { 38 | oninit: () => { 39 | rs.setBackgroundTask(getForums.load, 5000, () => { 40 | // return m.route.get() === '/files/files'; 41 | }); 42 | peopleUtil.ownIds((data) => { 43 | ownId = data; 44 | for (let i = 0; i < ownId.length; i++) { 45 | if (Number(ownId[i]) === 0) { 46 | ownId.splice(i, 1); 47 | } 48 | } 49 | ownId.unshift(0); 50 | }); 51 | }, 52 | view: (vnode) => 53 | m('.widget', [ 54 | m('.top-heading', [ 55 | m( 56 | 'button', 57 | { 58 | onclick: () => 59 | ownId && 60 | util.popupmessage( 61 | m(viewUtil.createforum, { 62 | authorId: ownId, 63 | }) 64 | ), 65 | }, 66 | 'Create Forum' 67 | ), 68 | m(util.SearchBar, { 69 | list: getForums.All, 70 | }), 71 | ]), 72 | Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mMsgId') // thread's view 73 | ? m(viewUtil.ThreadView, { 74 | msgId: vnode.attrs.pathInfo.mMsgId, 75 | forumId: vnode.attrs.pathInfo.mGroupId, 76 | }) 77 | : Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mGroupId') // Forum's view 78 | ? m(viewUtil.ForumView, { 79 | id: vnode.attrs.pathInfo.mGroupId, 80 | }) 81 | : m(sections[vnode.attrs.pathInfo.tab], { 82 | list: getForums[vnode.attrs.pathInfo.tab], 83 | }), 84 | ]), 85 | }; 86 | }; 87 | 88 | module.exports = { 89 | view: (vnode) => { 90 | return [ 91 | m(widget.Sidebar, { 92 | tabs: Object.keys(sections), 93 | baseRoute: '/forums/', 94 | }), 95 | m('.node-panel', m(Layout, { pathInfo: vnode.attrs })), 96 | ]; 97 | }, 98 | }; 99 | -------------------------------------------------------------------------------- /webui-src/app/forums/forums_util.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | 4 | const GROUP_SUBSCRIBE_ADMIN = 0x01; // means: you have the admin key for this group 5 | const GROUP_SUBSCRIBE_PUBLISH = 0x02; // means: you have the publish key for thiss group. Typical use: publish key in forums are shared with specific friends. 6 | const GROUP_SUBSCRIBE_SUBSCRIBED = 0x04; // means: you are subscribed to a group, which makes you a source for this group to your friend nodes. 7 | const GROUP_SUBSCRIBE_NOT_SUBSCRIBED = 0x08; 8 | const GROUP_MY_FORUM = GROUP_SUBSCRIBE_ADMIN + GROUP_SUBSCRIBE_SUBSCRIBED + GROUP_SUBSCRIBE_PUBLISH; 9 | 10 | const THREAD_UNREAD = 0x00000003; 11 | 12 | const Data = { 13 | DisplayForums: {}, 14 | Threads: {}, 15 | ParentThreads: {}, 16 | ParentThreadMap: {}, 17 | }; 18 | 19 | async function updatedisplayforums(keyid, details = {}) { 20 | const res = await rs.rsJsonApiRequest('/rsgxsforums/getForumsInfo', { 21 | forumIds: [keyid], // keyid: Forumid 22 | }); 23 | details = res.body.forumsInfo[0]; 24 | Data.DisplayForums[keyid] = { 25 | // struct for a forum 26 | name: details.mMeta.mGroupName, 27 | author: details.mMeta.mAuthorId, 28 | isSearched: true, 29 | description: details.mDescription, 30 | isSubscribed: 31 | details.mMeta.mSubscribeFlags === GROUP_SUBSCRIBE_SUBSCRIBED || 32 | details.mMeta.mSubscribeFlags === GROUP_MY_FORUM, 33 | activity: details.mMeta.mLastPost, 34 | created: details.mMeta.mPublishTs, 35 | }; 36 | if (Data.Threads[keyid] === undefined) { 37 | Data.Threads[keyid] = {}; 38 | } 39 | const res2 = await rs.rsJsonApiRequest('/rsgxsforums/getForumMsgMetaData', { 40 | forumId: keyid, 41 | }); 42 | if (res2.body.retval) { 43 | res2.body.msgMetas.map(async (thread) => { 44 | const res3 = await rs.rsJsonApiRequest('/rsgxsforums/getForumContent', { 45 | forumId: keyid, 46 | msgsIds: [thread.mMsgId], 47 | }); 48 | 49 | if ( 50 | res3.body.retval && 51 | (Data.Threads[keyid][thread.mOrigMsgId] === undefined || 52 | Data.Threads[keyid][thread.mOrigMsgId].thread.mMeta.mPublishTs.xint64 < 53 | thread.mPublishTs.xint64) 54 | // here we get the latest edited thread for each thread by comparing the publish time 55 | ) { 56 | Data.Threads[keyid][thread.mOrigMsgId] = { thread: res3.body.msgs[0], showReplies: false }; 57 | if ( 58 | Data.Threads[keyid][thread.mOrigMsgId] && 59 | Data.Threads[keyid][thread.mOrigMsgId].thread.mMeta.mMsgStatus === THREAD_UNREAD 60 | ) { 61 | let parent = Data.Threads[keyid][thread.mOrigMsgId].thread.mMeta.mParentId; 62 | while (Data.Threads[keyid][parent]) { 63 | // to mark all parent threads of an inread thread 64 | Data.Threads[keyid][parent].thread.mMeta.mMsgStatus = THREAD_UNREAD; 65 | parent = Data.Threads[keyid][parent].thread.mMeta.mParentId; 66 | } 67 | } 68 | 69 | if (Data.ParentThreads[keyid] === undefined) { 70 | Data.ParentThreads[keyid] = {}; 71 | } 72 | if (thread.mThreadId === thread.mParentId) { 73 | // top level thread. 74 | Data.ParentThreads[keyid][thread.mOrigMsgId] = 75 | Data.Threads[keyid][thread.mOrigMsgId].thread.mMeta; 76 | } else { 77 | if (Data.ParentThreadMap[thread.mParentId] === undefined) { 78 | Data.ParentThreadMap[thread.mParentId] = {}; 79 | } 80 | Data.ParentThreadMap[thread.mParentId][thread.mOrigMsgId] = thread; 81 | } 82 | } 83 | }); 84 | } 85 | } 86 | 87 | const DisplayForumsFromList = () => { 88 | return { 89 | view: (v) => 90 | m( 91 | 'tr', 92 | { 93 | key: v.attrs.id, 94 | class: 95 | Data.DisplayForums[v.attrs.id] && Data.DisplayForums[v.attrs.id].isSearched 96 | ? '' 97 | : 'hidden', 98 | onclick: () => { 99 | m.route.set('/forums/:tab/:mGroupId', { 100 | tab: v.attrs.category, 101 | mGroupId: v.attrs.id, 102 | }); 103 | }, 104 | }, 105 | [m('td', Data.DisplayForums[v.attrs.id] ? Data.DisplayForums[v.attrs.id].name : '')] 106 | ), 107 | }; 108 | }; 109 | 110 | const ForumSummary = () => { 111 | let keyid = {}; 112 | return { 113 | oninit: (v) => { 114 | keyid = v.attrs.details.mGroupId; 115 | updatedisplayforums(keyid); 116 | }, 117 | view: (v) => {}, 118 | }; 119 | }; 120 | 121 | const ForumTable = () => { 122 | return { 123 | view: (v) => m('table.forums', [m('tr', [m('th', 'Forum Name')]), v.children]), 124 | }; 125 | }; 126 | const ThreadsTable = () => { 127 | return { 128 | oninit: (v) => {}, 129 | view: (v) => 130 | m('table.threads', [ 131 | m('tr', [m('th', 'Comment'), m('th', 'Date'), m('th', 'Author')]), 132 | v.children, 133 | ]), 134 | }; 135 | }; 136 | const ThreadsReplyTable = () => { 137 | return { 138 | oninit: (v) => {}, 139 | view: (v) => 140 | m('table.threadreply', [ 141 | m('tr', [ 142 | m('th', ''), 143 | m('th', 'Comment'), 144 | m('th', 'Unread'), 145 | m('th', 'Author'), 146 | m('th', 'Date'), 147 | ]), 148 | v.children, 149 | ]), 150 | }; 151 | }; 152 | 153 | const SearchBar = () => { 154 | let searchString = ''; 155 | return { 156 | view: (v) => 157 | m('input[type=text][id=searchforum][placeholder=Search Subject].searchbar', { 158 | value: searchString, 159 | oninput: (e) => { 160 | searchString = e.target.value.toLowerCase(); 161 | for (const hash in Data.DisplayForums) { 162 | if (Data.DisplayForums[hash].name.toLowerCase().indexOf(searchString) > -1) { 163 | Data.DisplayForums[hash].isSearched = true; 164 | } else { 165 | Data.DisplayForums[hash].isSearched = false; 166 | } 167 | } 168 | }, 169 | }), 170 | }; 171 | }; 172 | function popupmessage(message) { 173 | const container = document.getElementById('modal-container'); 174 | container.style.display = 'block'; 175 | m.render( 176 | container, 177 | m('.modal-content', [ 178 | m( 179 | 'button.red', 180 | { 181 | onclick: () => (container.style.display = 'none'), 182 | }, 183 | m('i.fas.fa-times') 184 | ), 185 | message, 186 | ]) 187 | ); 188 | } 189 | 190 | module.exports = { 191 | Data, 192 | SearchBar, 193 | ForumSummary, 194 | DisplayForumsFromList, 195 | ForumTable, 196 | ThreadsTable, 197 | ThreadsReplyTable, 198 | popupmessage, 199 | updatedisplayforums, 200 | GROUP_SUBSCRIBE_ADMIN, 201 | GROUP_SUBSCRIBE_NOT_SUBSCRIBED, 202 | GROUP_SUBSCRIBE_PUBLISH, 203 | GROUP_SUBSCRIBE_SUBSCRIBED, 204 | GROUP_MY_FORUM, 205 | THREAD_UNREAD, 206 | }; 207 | -------------------------------------------------------------------------------- /webui-src/app/forums/my_forums.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('forums/forums_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'My Forums')), 8 | m('.widget__body', [ 9 | m( 10 | util.ForumTable, 11 | m('tbody', [ 12 | v.attrs.list.map((forum) => 13 | m(util.ForumSummary, { 14 | details: forum, 15 | category: 'MyForums', 16 | }) 17 | ), 18 | v.attrs.list.map((forum) => 19 | m(util.DisplayForumsFromList, { 20 | id: forum.mGroupId, 21 | category: 'MyForums', 22 | }) 23 | ), 24 | ]) 25 | ), 26 | ]), 27 | ], 28 | }; 29 | }; 30 | 31 | module.exports = Layout; 32 | -------------------------------------------------------------------------------- /webui-src/app/forums/other_forums.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | 3 | const Layout = () => { 4 | return { 5 | view: () => [m('.widget__heading', m('h3', 'Other Forums'))], 6 | }; 7 | }; 8 | 9 | module.exports = Layout(); 10 | -------------------------------------------------------------------------------- /webui-src/app/forums/popular_forums.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('forums/forums_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Popular Forums')), 8 | m('.widget__body', [ 9 | m( 10 | util.ForumTable, 11 | m('tbody', [ 12 | v.attrs.list.map((forum) => 13 | m(util.ForumSummary, { 14 | details: forum, 15 | category: 'PopularForums', 16 | }) 17 | ), 18 | v.attrs.list.map((forum) => 19 | m(util.DisplayForumsFromList, { 20 | id: forum.mGroupId, 21 | category: 'PopularForums', 22 | }) 23 | ), 24 | ]) 25 | ), 26 | ]), 27 | ], 28 | }; 29 | }; 30 | 31 | module.exports = Layout; 32 | -------------------------------------------------------------------------------- /webui-src/app/forums/subscribed_forums.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('forums/forums_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Subscribed Forums')), 8 | m('.widget__body', [ 9 | m( 10 | util.ForumTable, 11 | m('tbody', [ 12 | v.attrs.list.map((forum) => 13 | m(util.ForumSummary, { 14 | details: forum, 15 | category: 'SubscribedForums', 16 | }) 17 | ), 18 | v.attrs.list.map((forum) => 19 | m(util.DisplayForumsFromList, { 20 | id: forum.mGroupId, 21 | category: 'SubscribedForums', 22 | }) 23 | ), 24 | ]) 25 | ), 26 | ]), 27 | ], 28 | }; 29 | }; 30 | 31 | module.exports = Layout; 32 | -------------------------------------------------------------------------------- /webui-src/app/login.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | 4 | const displayErrorMessage = function (message) { 5 | m.render(document.getElementById('error'), message); 6 | }; 7 | 8 | const verifyLogin = async function (uname, passwd, url, displayAuthError = true) { 9 | const loginHeader = { 10 | Authorization: `Basic ${btoa(`${uname}:${passwd}`)}`, 11 | }; 12 | if (!url.trim()) { 13 | displayErrorMessage('Server-url is missing, please enter json-api url'); 14 | return; 15 | } 16 | rs.setKeys('', '', url, false); 17 | rs.logon( 18 | loginHeader, 19 | displayAuthError ? displayErrorMessage : () => {}, 20 | displayErrorMessage, 21 | () => { 22 | rs.setKeys(uname, passwd, url); 23 | m.route.set('/home'); 24 | } 25 | ); 26 | }; 27 | 28 | function loginComponent() { 29 | const urlParams = new URLSearchParams(window.location.search); 30 | let uname = urlParams.get('Username') || 'webui'; 31 | let passwd = urlParams.get('Password') || ''; 32 | let url = 33 | urlParams.get('Url') || window.location.protocol === 'file:' 34 | ? 'http://127.0.0.1:9092' 35 | : window.location.protocol + 36 | '//' + 37 | window.location.host + 38 | window.location.pathname.replace('/index.html', ''); 39 | let withOptions = false; 40 | 41 | const logo = () => 42 | m('img.logo[width=30%]', { src: 'images/retroshare.svg', alt: 'retroshare_icon' }); 43 | 44 | const inputName = () => 45 | m('input', { 46 | id: 'username', 47 | type: 'text', 48 | value: uname, 49 | placeholder: 'Username', 50 | onchange: (e) => (uname = e.target.value), 51 | }); 52 | const buttonLogin = () => 53 | m( 54 | 'button[type=submit].submit-btn#loginBtn', 55 | { 56 | onclick: (ev) => { 57 | ev.preventDefault(); 58 | verifyLogin(uname, passwd, url); 59 | }, 60 | }, 61 | 'Login' 62 | ); 63 | 64 | const inputPassword = () => 65 | m('input[autofocus]', { 66 | id: 'password', 67 | type: 'password', 68 | placeholder: 'Password', 69 | oncreate: (e) => e.dom.focus(), 70 | onchange: (e) => (passwd = e.target.value), 71 | }); 72 | 73 | const inputUrl = () => 74 | m('input', { 75 | id: 'url', 76 | type: 'text', 77 | placeholder: 'Url', 78 | value: url, 79 | oninput: (e) => (url = e.target.value), 80 | }); 81 | 82 | const linkOptions = (action) => 83 | m('a', { onclick: () => (withOptions = !withOptions) }, `${action} options`); 84 | 85 | const textError = () => m('p.error[id=error]'); 86 | return { 87 | view: () => { 88 | return m( 89 | 'form.login-page', 90 | m( 91 | '.login-container', 92 | withOptions 93 | ? [ 94 | logo(), 95 | m('.extra', [m('label', 'Username:'), m('br'), inputName()]), 96 | m('.extra', [m('label', 'Password:'), m('br'), inputPassword()]), 97 | m('.extra', [m('label', 'Url:'), m('br'), inputUrl()]), 98 | linkOptions('hide'), 99 | buttonLogin(), 100 | textError(), 101 | ] 102 | : [logo(), inputPassword(), linkOptions('show'), buttonLogin(), textError()] 103 | ) 104 | ); 105 | }, 106 | }; 107 | } 108 | 109 | module.exports = loginComponent; 110 | -------------------------------------------------------------------------------- /webui-src/app/mail/mail_attachment.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | const util = require('mail/mail_util'); 4 | 5 | const Layout = () => { 6 | const files = []; 7 | let viewChanged = false; 8 | 9 | return { 10 | oninit: (v) => { 11 | v.attrs.list.forEach(async (element) => { 12 | const res = await rs.rsJsonApiRequest('/rsMsgs/getMessage', { 13 | msgId: element.msgId, 14 | }); 15 | if (res.body.retval) { 16 | res.body.msg.files.forEach((element) => { 17 | files.push({ ...element, from: res.body.msg.from, ts: res.body.msg.ts }); 18 | }); 19 | } 20 | }); 21 | }, 22 | view: (v) => [ 23 | m('.widget__heading', [ 24 | m('h3', 'Attachments'), 25 | m('.view-toggle', [ 26 | m( 27 | '.mail-view', 28 | { 29 | onclick: () => (viewChanged = false), 30 | style: { backgroundColor: viewChanged ? '#fff' : '#019DFF' }, 31 | }, 32 | m('i.fas.fa-mail-bulk') 33 | ), 34 | m( 35 | '.attachment-view', 36 | { 37 | onclick: () => (viewChanged = true), 38 | style: { backgroundColor: viewChanged ? '#019DFF' : '#fff' }, 39 | }, 40 | m('i.fas.fa-file') 41 | ), 42 | ]), 43 | ]), 44 | m('.widget__body', [ 45 | viewChanged 46 | ? m(util.AttachmentSection, { 47 | files, 48 | }) 49 | : m( 50 | util.Table, 51 | m( 52 | 'tbody', 53 | v.attrs.list.map((msg) => 54 | m(util.MessageSummary, { 55 | details: msg, 56 | category: 'attachment', 57 | }) 58 | ) 59 | ) 60 | ), 61 | ]), 62 | ], 63 | }; 64 | }; 65 | 66 | module.exports = Layout; 67 | -------------------------------------------------------------------------------- /webui-src/app/mail/mail_draftbox.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | 3 | const util = require('mail/mail_util'); 4 | 5 | const Layout = () => { 6 | return { 7 | view: (v) => [ 8 | m('.widget__heading', m('h3', 'Draft')), 9 | m('.widget__body', [ 10 | m( 11 | util.Table, 12 | m( 13 | 'tbody', 14 | v.attrs.list.map((msg) => 15 | m(util.MessageSummary, { 16 | details: msg, 17 | category: 'drafts', 18 | }) 19 | ) 20 | ) 21 | ), 22 | ]), 23 | ], 24 | }; 25 | }; 26 | 27 | module.exports = Layout; 28 | -------------------------------------------------------------------------------- /webui-src/app/mail/mail_important.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('mail/mail_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Important')), 8 | m('.widget__body', [ 9 | m( 10 | util.Table, 11 | m( 12 | 'tbody', 13 | v.attrs.list.map((msg) => 14 | m(util.MessageSummary, { 15 | details: msg, 16 | category: 'important', 17 | }) 18 | ) 19 | ) 20 | ), 21 | ]), 22 | ], 23 | }; 24 | }; 25 | 26 | module.exports = Layout; 27 | -------------------------------------------------------------------------------- /webui-src/app/mail/mail_inbox.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('mail/mail_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Inbox')), 8 | m('.widget__body', [ 9 | m( 10 | util.Table, 11 | m( 12 | 'tbody', 13 | v.attrs.list.map((msg) => 14 | m(util.MessageSummary, { 15 | details: msg, 16 | category: 'inbox', 17 | }) 18 | ) 19 | ) 20 | ), 21 | ]), 22 | ], 23 | }; 24 | }; 25 | 26 | module.exports = Layout; 27 | -------------------------------------------------------------------------------- /webui-src/app/mail/mail_later.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('mail/mail_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Later')), 8 | m('.widget__body', [ 9 | m( 10 | util.Table, 11 | m( 12 | 'tbody', 13 | v.attrs.list.map((msg) => 14 | m(util.MessageSummary, { 15 | details: msg, 16 | category: 'later', 17 | }) 18 | ) 19 | ) 20 | ), 21 | ]), 22 | ], 23 | }; 24 | }; 25 | 26 | module.exports = Layout; 27 | -------------------------------------------------------------------------------- /webui-src/app/mail/mail_outbox.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('mail/mail_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Outbox')), 8 | m('.widget__body', [ 9 | m( 10 | util.Table, 11 | m( 12 | 'tbody', 13 | v.attrs.list.map((msg) => 14 | m(util.MessageSummary, { 15 | details: msg, 16 | category: 'outbox', 17 | }) 18 | ) 19 | ) 20 | ), 21 | ]), 22 | ], 23 | }; 24 | }; 25 | 26 | module.exports = Layout; 27 | -------------------------------------------------------------------------------- /webui-src/app/mail/mail_personal.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('mail/mail_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Personal')), 8 | m('.widget__body', [ 9 | m( 10 | util.Table, 11 | m( 12 | 'tbody', 13 | v.attrs.list.map((msg) => 14 | m(util.MessageSummary, { 15 | details: msg, 16 | category: 'personal', 17 | }) 18 | ) 19 | ) 20 | ), 21 | ]), 22 | ], 23 | }; 24 | }; 25 | 26 | module.exports = Layout; 27 | -------------------------------------------------------------------------------- /webui-src/app/mail/mail_resolver.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | const util = require('mail/mail_util'); 4 | const compose = require('mail/mail_compose'); 5 | 6 | const Messages = { 7 | all: [], 8 | inbox: [], 9 | sent: [], 10 | outbox: [], 11 | drafts: [], 12 | trash: [], 13 | starred: [], 14 | system: [], 15 | spam: [], 16 | attachment: [], 17 | important: [], 18 | work: [], 19 | personal: [], 20 | todo: [], 21 | later: [], 22 | load() { 23 | rs.rsJsonApiRequest('/rsMsgs/getMessageSummaries', { box: util.BOX_ALL }, (data) => { 24 | Messages.all = data.msgList; 25 | Messages.inbox = Messages.all.filter( 26 | (msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_INBOX 27 | ); 28 | Messages.sent = Messages.all.filter( 29 | (msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_SENTBOX 30 | ); 31 | Messages.outbox = Messages.all.filter( 32 | (msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_OUTBOX 33 | ); 34 | Messages.drafts = Messages.all.filter( 35 | (msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_DRAFTBOX 36 | ); 37 | Messages.trash = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_TRASH); 38 | Messages.starred = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_STAR); 39 | Messages.system = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_SYSTEM); 40 | Messages.spam = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_SPAM); 41 | 42 | Messages.attachment = Messages.all.filter((msg) => msg.count); 43 | 44 | // Messages.important = Messages.all.filter( 45 | // (msg) => msg.msgflags & util.RS_MSGTAGTYPE_IMPORTANT 46 | // ); 47 | // Messages.work = Messages.all.filter((msg) => msg.msgflags & util.RS_MSGTAGTYPE_WORK); 48 | // Messages.personal = Messages.all.filter((msg) => msg.msgflags & util.RS_MSGTAGTYPE_PERSONAL); 49 | // Messages.todo = Messages.all.filter((msg) => msg.msgflags & util.RS_MSGTAGTYPE_TODO); 50 | // Messages.later = Messages.all.filter((msg) => msg.msgflags & util.RS_MSGTAGTYPE_LATER); 51 | }); 52 | }, 53 | }; 54 | 55 | const sections = { 56 | inbox: require('mail/mail_inbox'), 57 | outbox: require('mail/mail_outbox'), 58 | drafts: require('mail/mail_draftbox'), 59 | sent: require('mail/mail_sentbox'), 60 | trash: require('mail/mail_trashbox'), 61 | }; 62 | const sectionsquickview = { 63 | starred: require('mail/mail_starred'), 64 | system: require('mail/mail_system'), 65 | spam: require('mail/mail_spam'), 66 | attachment: require('mail/mail_attachment'), 67 | important: require('mail/mail_important'), 68 | work: require('mail/mail_work'), 69 | todo: require('mail/mail_todo'), 70 | later: require('mail/mail_later'), 71 | personal: require('mail/mail_personal'), 72 | }; 73 | const tagselect = { 74 | showval: 'Tags', 75 | opts: ['Tags', 'Important', 'Work', 'Personal'], 76 | }; 77 | const Layout = () => { 78 | let showCompose = false; 79 | // setFunction like react to show/hide popup 80 | function setShowCompose(bool) { 81 | showCompose = bool; 82 | } 83 | return { 84 | oninit: () => Messages.load(), 85 | view: (vnode) => { 86 | const sectionsSize = { 87 | inbox: Messages.inbox.length, 88 | outbox: Messages.outbox.length, 89 | drafts: Messages.drafts.length, 90 | sent: Messages.sent.length, 91 | trash: Messages.trash.length, 92 | }; 93 | const sectionsQuickviewSize = { 94 | starred: Messages.starred.length, 95 | system: Messages.system.length, 96 | spam: Messages.spam.length, 97 | attachment: Messages.attachment.length, 98 | important: Messages.important.length, 99 | work: Messages.work.length, 100 | todo: Messages.todo.length, 101 | later: Messages.later.length, 102 | personal: Messages.personal.length, 103 | }; 104 | 105 | return [ 106 | m('.side-bar', [ 107 | m('button.mail-compose-btn', { onclick: () => setShowCompose(true) }, 'Compose'), 108 | m(util.Sidebar, { 109 | tabs: Object.keys(sections), 110 | size: sectionsSize, 111 | baseRoute: '/mail/', 112 | }), 113 | m(util.SidebarQuickView, { 114 | tabs: Object.keys(sectionsquickview), 115 | size: sectionsQuickviewSize, 116 | baseRoute: '/mail/', 117 | }), 118 | ]), 119 | m( 120 | '.node-panel', 121 | m('.widget', [ 122 | m.route.get().split('/').length < 4 && 123 | m('.top-heading', [ 124 | m( 125 | 'select.mail-tag', 126 | { 127 | value: tagselect.showval, 128 | onchange: (e) => (tagselect.showval = tagselect.opts[e.target.selectedIndex]), 129 | }, 130 | [tagselect.opts.map((opt) => m('option', { value: opt }, opt.toLocaleString()))] 131 | ), 132 | m(util.SearchBar, { list: {} }), 133 | ]), 134 | vnode.children, 135 | ]) 136 | ), 137 | m( 138 | '.composePopupOverlay#mailComposerPopup', 139 | { style: { display: showCompose ? 'block' : 'none' } }, 140 | m( 141 | '.composePopup', 142 | m(compose, { msgType: 'compose', setShowCompose }), 143 | m('button.red.close-btn', { onclick: () => setShowCompose(false) }, m('i.fas.fa-times')) 144 | ) 145 | ), 146 | ]; 147 | }, 148 | }; 149 | }; 150 | 151 | module.exports = { 152 | view: ({ attrs, attrs: { tab, msgId } }) => { 153 | // TODO: utilize multiple routing params 154 | if (Object.prototype.hasOwnProperty.call(attrs, 'msgId')) { 155 | return m(Layout, m(util.MessageView, { msgId })); 156 | } 157 | return m( 158 | Layout, 159 | m(sections[tab] || sectionsquickview[tab], { 160 | list: Messages[tab].sort((msgA, msgB) => { 161 | const msgADate = new Date(msgA.ts.xint64 * 1000); 162 | const msgBDate = new Date(msgB.ts.xint64 * 1000); 163 | return msgADate < msgBDate; 164 | }), 165 | }) 166 | ); 167 | }, 168 | }; 169 | -------------------------------------------------------------------------------- /webui-src/app/mail/mail_sentbox.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | 3 | const util = require('mail/mail_util'); 4 | 5 | const Layout = () => { 6 | return { 7 | view: (v) => [ 8 | m('.widget__heading', m('h3', 'Sent')), 9 | m('.widget__body', [ 10 | m( 11 | util.Table, 12 | m( 13 | 'tbody', 14 | v.attrs.list.map((msg) => 15 | m(util.MessageSummary, { 16 | details: msg, 17 | category: 'sent', 18 | }) 19 | ) 20 | ) 21 | ), 22 | ]), 23 | ], 24 | }; 25 | }; 26 | 27 | module.exports = Layout; 28 | -------------------------------------------------------------------------------- /webui-src/app/mail/mail_spam.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('mail/mail_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Spam')), 8 | m('.widget__body', [ 9 | m( 10 | util.Table, 11 | m( 12 | 'tbody', 13 | v.attrs.list.map((msg) => 14 | m(util.MessageSummary, { 15 | details: msg, 16 | category: 'spam', 17 | }) 18 | ) 19 | ) 20 | ), 21 | ]), 22 | ], 23 | }; 24 | }; 25 | 26 | module.exports = Layout; 27 | -------------------------------------------------------------------------------- /webui-src/app/mail/mail_starred.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('mail/mail_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Starred')), 8 | m('.widget__body', [ 9 | m( 10 | util.Table, 11 | m( 12 | 'tbody', 13 | v.attrs.list.map((msg) => 14 | m(util.MessageSummary, { 15 | details: msg, 16 | category: 'starred', 17 | }) 18 | ) 19 | ) 20 | ), 21 | ]), 22 | ], 23 | }; 24 | }; 25 | 26 | module.exports = Layout; 27 | -------------------------------------------------------------------------------- /webui-src/app/mail/mail_system.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('mail/mail_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'System')), 8 | m('.widget__body', [ 9 | m( 10 | util.Table, 11 | m( 12 | 'tbody', 13 | v.attrs.list.map((msg) => 14 | m(util.MessageSummary, { 15 | details: msg, 16 | category: 'system', 17 | }) 18 | ) 19 | ) 20 | ), 21 | ]), 22 | ], 23 | }; 24 | }; 25 | 26 | module.exports = Layout; 27 | -------------------------------------------------------------------------------- /webui-src/app/mail/mail_todo.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('mail/mail_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Todo')), 8 | m('.widget__body', [ 9 | m( 10 | util.Table, 11 | m( 12 | 'tbody', 13 | v.attrs.list.map((msg) => 14 | m(util.MessageSummary, { 15 | details: msg, 16 | category: 'todo', 17 | }) 18 | ) 19 | ) 20 | ), 21 | ]), 22 | ], 23 | }; 24 | }; 25 | 26 | module.exports = Layout; 27 | -------------------------------------------------------------------------------- /webui-src/app/mail/mail_trashbox.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('mail/mail_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Trash')), 8 | m('.widget__body', [ 9 | m( 10 | util.Table, 11 | m( 12 | 'tbody', 13 | v.attrs.list.map((msg) => 14 | m(util.MessageSummary, { 15 | details: msg, 16 | category: 'trash', 17 | }) 18 | ) 19 | ) 20 | ), 21 | ]), 22 | ], 23 | }; 24 | }; 25 | 26 | module.exports = Layout; 27 | -------------------------------------------------------------------------------- /webui-src/app/mail/mail_work.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const util = require('mail/mail_util'); 3 | 4 | const Layout = () => { 5 | return { 6 | view: (v) => [ 7 | m('.widget__heading', m('h3', 'Work')), 8 | m('.widget__body', [ 9 | m( 10 | util.Table, 11 | m( 12 | 'tbody', 13 | v.attrs.list.map((msg) => 14 | m(util.MessageSummary, { 15 | details: msg, 16 | category: 'work', 17 | }) 18 | ) 19 | ) 20 | ), 21 | ]), 22 | ], 23 | }; 24 | }; 25 | 26 | module.exports = Layout; 27 | -------------------------------------------------------------------------------- /webui-src/app/main.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | 3 | const login = require('login'); 4 | const home = require('home'); 5 | const network = require('network/network'); 6 | const people = require('people/people_resolver'); 7 | const chat = require('chat/chat'); 8 | const mail = require('mail/mail_resolver'); 9 | const files = require('files/files_resolver'); 10 | const channels = require('channels/channels'); 11 | const forums = require('forums/forums'); 12 | const boards = require('boards/boards'); 13 | const config = require('config/config_resolver'); 14 | 15 | const navIcon = { 16 | home: m('i.fas.fa-home.sidenav-icon'), 17 | network: m('i.fas.fa-share-alt.sidenav-icon'), 18 | people: m('i.fas.fa-users.sidenav-icon'), 19 | chat: m('i.fas.fa-comments.sidenav-icon'), 20 | mail: m('i.fas.fa-envelope.sidenav-icon'), 21 | files: m('i.fas.fa-folder-open.sidenav-icon'), 22 | channels: m('i.fas.fa-tv.sidenav-icon'), 23 | forums: m('i.fas.fa-bullhorn.sidenav-icon'), 24 | boards: m('i.fas.fa-globe.sidenav-icon'), 25 | config: m('i.fas.fa-cogs.sidenav-icon'), 26 | }; 27 | 28 | const navbar = () => { 29 | let isCollapsed = true; 30 | return { 31 | view: (vnode) => 32 | m( 33 | 'nav.nav-menu', 34 | { 35 | class: isCollapsed ? 'collapsed' : '', 36 | }, 37 | [ 38 | m('.nav-menu__logo', [ 39 | m('img', { 40 | src: 'images/retroshare.svg', 41 | alt: 'retroshare_icon', 42 | }), 43 | m('h5', 'Retroshare'), 44 | ]), 45 | m('.nav-menu__box', [ 46 | Object.keys(vnode.attrs.links).map((linkName, i) => { 47 | const active = m.route.get().split('/')[1] === linkName; 48 | return m( 49 | m.route.Link, 50 | { 51 | href: vnode.attrs.links[linkName], 52 | class: 'item' + (active ? ' item-selected' : ''), 53 | }, 54 | [navIcon[linkName], m('p', linkName)] 55 | ); 56 | }), 57 | m( 58 | 'button.toggle-nav', 59 | { 60 | onclick: () => (isCollapsed = !isCollapsed), 61 | }, 62 | m('i.fas.fa-angle-double-left') 63 | ), 64 | ]), 65 | ] 66 | ), 67 | }; 68 | }; 69 | 70 | const Layout = () => { 71 | return { 72 | view: (vnode) => 73 | m('.content', [ 74 | m(navbar, { 75 | links: { 76 | home: '/home', 77 | network: '/network', 78 | people: '/people/OwnIdentity', 79 | chat: '/chat', 80 | mail: '/mail/inbox', 81 | files: '/files/files', 82 | channels: '/channels/MyChannels', 83 | forums: '/forums/MyForums', 84 | boards: '/boards/MyBoards', 85 | config: '/config/network', 86 | }, 87 | }), 88 | m('.tab-content', vnode.children), 89 | ]), 90 | }; 91 | }; 92 | 93 | m.route(document.getElementById('main'), '/', { 94 | '/': { 95 | render: () => m(login), 96 | }, 97 | '/home': { 98 | render: () => m(Layout, m(home)), 99 | }, 100 | '/network': { 101 | render: () => m(Layout, m(network)), 102 | }, 103 | 104 | '/people/:tab': { 105 | render: (v) => m(Layout, m(people, v.attrs)), 106 | }, 107 | '/chat/:lobby/:subaction': { 108 | render: (v) => m(Layout, m(chat, v.attrs)), 109 | }, 110 | '/chat/:lobby': { 111 | render: (v) => m(Layout, m(chat, v.attrs)), 112 | }, 113 | '/chat': { 114 | render: () => m(Layout, m(chat)), 115 | }, 116 | '/mail/:tab': { 117 | render: (v) => m(Layout, m(mail, v.attrs)), 118 | }, 119 | '/mail/:tab/:msgId': { 120 | render: (v) => m(Layout, m(mail, v.attrs)), 121 | }, 122 | '/files/:tab': { 123 | render: (v) => m(Layout, m(files, v.attrs)), 124 | }, 125 | '/files/:tab/:resultId': { 126 | render: (v) => m(Layout, m(files, v.attrs)), 127 | }, 128 | '/channels/:tab': { 129 | render: (v) => m(Layout, m(channels, v.attrs)), 130 | }, 131 | '/channels/:tab/:mGroupId': { 132 | render: (v) => m(Layout, m(channels, v.attrs)), 133 | }, 134 | '/channels/:tab/:mGroupId/:mMsgId': { 135 | render: (v) => m(Layout, m(channels, v.attrs)), 136 | }, 137 | '/forums/:tab': { 138 | render: (v) => m(Layout, m(forums, v.attrs)), 139 | }, 140 | '/forums/:tab/:mGroupId': { 141 | render: (v) => m(Layout, m(forums, v.attrs)), 142 | }, 143 | 144 | '/forums/:tab/:mGroupId/:mMsgId': { 145 | render: (v) => m(Layout, m(forums, v.attrs)), 146 | }, 147 | '/boards/:tab': { 148 | render: (v) => m(Layout, m(boards, v.attrs)), 149 | }, 150 | '/boards/:tab/:mGroupId': { 151 | render: (v) => m(Layout, m(boards, v.attrs)), 152 | }, 153 | '/boards/:tab/:mGroupId/:mMsgId': { 154 | render: (v) => m(Layout, m(boards, v.attrs)), 155 | }, 156 | '/config/:tab': { 157 | render: (v) => m(Layout, m(config, v.attrs)), 158 | }, 159 | }); 160 | -------------------------------------------------------------------------------- /webui-src/app/network/network.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | const widget = require('widgets'); 4 | const Data = require('network/network_data'); 5 | 6 | const ConfirmRemove = () => { 7 | return { 8 | view: (vnode) => [ 9 | m('h3', 'Remove Friend'), 10 | m('hr'), 11 | m('p', 'Are you sure you want to end connections with this node?'), 12 | m( 13 | 'button', 14 | { 15 | onclick: () => { 16 | rs.rsJsonApiRequest('/rsPeers/removeFriend', { 17 | pgpId: vnode.attrs.gpg_id, 18 | }); 19 | m.redraw(); 20 | }, 21 | }, 22 | 'Confirm' 23 | ), 24 | ], 25 | }; 26 | }; 27 | 28 | const Locations = () => { 29 | return { 30 | view: (v) => [ 31 | m('h4', 'Locations'), 32 | v.attrs.locations.map((loc) => 33 | m('.location', [ 34 | m('i.fas.fa-user-tag', { style: 'margin-top:3px' }), 35 | m('span', { style: 'margin-top:1px' }, loc.name), 36 | m('p', 'ID :'), 37 | m('p', loc.id), 38 | m('p', 'Last contacted :'), 39 | m('p', new Date(loc.lastSeen * 1000).toDateString()), 40 | m('p', 'Online :'), 41 | m('i.fas', { 42 | class: loc.isOnline ? 'fa-check-circle' : 'fa-times-circle', 43 | }), 44 | m( 45 | 'button.red', 46 | { 47 | onclick: () => 48 | widget.popupMessage( 49 | m(ConfirmRemove, { 50 | gpg: loc.gpg_id, 51 | }) 52 | ), 53 | }, 54 | 'Remove node' 55 | ), 56 | ]) 57 | ), 58 | ], 59 | }; 60 | }; 61 | 62 | const Friend = () => { 63 | return { 64 | isExpanded: false, 65 | 66 | view: (vnode) => 67 | m( 68 | '.friend', 69 | { 70 | key: vnode.attrs.id, 71 | class: Data.gpgDetails[vnode.attrs.id].isSearched ? '' : 'hidden', 72 | }, 73 | [ 74 | m('i.fas.fa-angle-right', { 75 | class: 'fa-rotate-' + (vnode.state.isExpanded ? '90' : '0'), 76 | style: 'margin-top:12px', 77 | onclick: () => (vnode.state.isExpanded = !vnode.state.isExpanded), 78 | }), 79 | m('.brief-info', { class: Data.gpgDetails[vnode.attrs.id].isOnline ? 'online' : '' }, [ 80 | m('i.fas.fa-2x.fa-user-circle'), 81 | m('span', Data.gpgDetails[vnode.attrs.id].name), 82 | ]), 83 | m( 84 | '.details', 85 | { 86 | style: 'display:' + (vnode.state.isExpanded ? 'block' : 'none'), 87 | }, 88 | [ 89 | m(Locations, { 90 | locations: Data.gpgDetails[vnode.attrs.id].locations, 91 | }), 92 | ] 93 | ), 94 | ] 95 | ), 96 | }; 97 | }; 98 | 99 | const SearchBar = () => { 100 | let searchString = ''; 101 | return { 102 | view: () => 103 | m('input.searchbar', { 104 | type: 'text', 105 | placeholder: 'search', 106 | value: searchString, 107 | oninput: (e) => { 108 | searchString = e.target.value.toLowerCase(); 109 | for (const id in Data.gpgDetails) { 110 | if (Data.gpgDetails[id].name.toLowerCase().indexOf(searchString) > -1) { 111 | Data.gpgDetails[id].isSearched = true; 112 | } else { 113 | Data.gpgDetails[id].isSearched = false; 114 | } 115 | } 116 | }, 117 | }), 118 | }; 119 | }; 120 | 121 | const FriendsList = () => { 122 | return { 123 | oninit: () => { 124 | Data.refreshGpgDetails(); 125 | }, 126 | view: () => 127 | m('.widget', [ 128 | m('.widget__heading', [m('h3', 'Friend nodes'), m(SearchBar)]), 129 | m('.widget__body', [ 130 | Object.entries(Data.gpgDetails) 131 | .sort((a, b) => { 132 | return a[1].isOnline === b[1].isOnline ? 0 : a[1].isOnline ? -1 : 1; 133 | }) 134 | .map((item) => { 135 | const id = item[0]; 136 | return m(Friend, { id }); 137 | }), 138 | ]), 139 | ]), 140 | }; 141 | }; 142 | 143 | const Layout = () => { 144 | return { 145 | view: () => m('.node-panel', m(FriendsList)), 146 | }; 147 | }; 148 | 149 | module.exports = Layout; 150 | -------------------------------------------------------------------------------- /webui-src/app/network/network_data.js: -------------------------------------------------------------------------------- 1 | const rs = require('rswebui'); 2 | 3 | async function refreshIds() { 4 | let sslIds = []; 5 | await rs.rsJsonApiRequest('/rsPeers/getFriendList', {}, (data) => (sslIds = data.sslIds)); 6 | return sslIds; 7 | } 8 | 9 | async function loadSslDetails() { 10 | const sslDetails = []; 11 | const sslIds = await refreshIds(); 12 | await Promise.all( 13 | sslIds.map((sslId) => 14 | rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { sslId }, (data) => sslDetails.push(data.det)) 15 | ) 16 | ); 17 | return sslDetails; 18 | } 19 | 20 | const Data = { 21 | gpgDetails: {}, 22 | }; 23 | Data.refreshGpgDetails = async function () { 24 | const details = {}; 25 | const sslDetails = await loadSslDetails(); 26 | await Promise.all( 27 | sslDetails.map((data) => { 28 | let isOnline = false; 29 | return rs 30 | .rsJsonApiRequest( 31 | '/rsPeers/isOnline', 32 | { sslId: data.id }, 33 | (stat) => (isOnline = stat.retval) 34 | ) 35 | .then(() => { 36 | const loc = { 37 | name: data.location, 38 | id: data.id, 39 | lastSeen: data.lastConnect, 40 | isOnline, 41 | gpg_id: data.gpg_id, 42 | }; 43 | 44 | if (details[data.gpg_id] === undefined) { 45 | details[data.gpg_id] = { 46 | name: data.name, 47 | isSearched: true, 48 | isOnline, 49 | locations: [loc], 50 | }; 51 | } else { 52 | details[data.gpg_id].locations.push(loc); 53 | } 54 | details[data.gpg_id].isOnline = details[data.gpg_id].isOnline || isOnline; 55 | }); 56 | }) 57 | ); 58 | Data.gpgDetails = details; 59 | }; 60 | module.exports = Data; 61 | -------------------------------------------------------------------------------- /webui-src/app/people/people.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | 4 | const peopleUtil = require('people/people_util'); 5 | 6 | const AllContacts = () => { 7 | const list = peopleUtil.sortUsers(rs.userList.users); 8 | return { 9 | view: () => { 10 | return m('.widget', [ 11 | m('.widget__heading', [ 12 | m('h3', 'Contacts', m('span.counter', list.length)), 13 | m(peopleUtil.SearchBar), 14 | ]), 15 | m('.widget__body', [list.map((id) => m(peopleUtil.regularcontactInfo, { id }))]), 16 | ]); 17 | }, 18 | }; 19 | }; 20 | 21 | module.exports = { 22 | view: () => { 23 | return m(AllContacts); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /webui-src/app/people/people_own_contacts.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const rs = require('rswebui'); 3 | const peopleUtil = require('people/people_util'); 4 | 5 | const MyContacts = () => { 6 | const list = peopleUtil.contactlist(rs.userList.users); 7 | return { 8 | view: () => { 9 | return m('.widget', [ 10 | m('.widget__heading', [ 11 | m('h3', 'MyContacts', m('span.counter', list.length)), 12 | m(peopleUtil.SearchBar), 13 | ]), 14 | m('.widget__body', [list.map((id) => m(peopleUtil.regularcontactInfo, { id }))]), 15 | ]); 16 | }, 17 | }; 18 | }; 19 | 20 | module.exports = { 21 | view: () => { 22 | return m(MyContacts); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /webui-src/app/people/people_resolver.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const widget = require('widgets'); 3 | 4 | const sections = { 5 | OwnIdentity: require('people/people_ownids'), 6 | MyContacts: require('people/people_own_contacts'), 7 | All: require('people/people'), 8 | }; 9 | 10 | const Layout = { 11 | view: (vnode) => [ 12 | m(widget.Sidebar, { 13 | tabs: Object.keys(sections), 14 | baseRoute: '/people/', 15 | }), 16 | m('.node-panel .', vnode.children), 17 | ], 18 | }; 19 | 20 | module.exports = { 21 | view: (vnode) => { 22 | const tab = vnode.attrs.tab; 23 | return m(Layout, m(sections[tab])); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /webui-src/app/people/people_util.js: -------------------------------------------------------------------------------- 1 | const rs = require('rswebui'); 2 | const m = require('mithril'); 3 | 4 | function checksudo(id) { 5 | return id === '0000000000000000'; 6 | } 7 | 8 | const UserAvatar = () => ({ 9 | view: (v) => { 10 | const imageURI = v.attrs.avatar; 11 | return imageURI === undefined || imageURI.mData.base64 === '' 12 | ? m( 13 | 'div.defaultAvatar', 14 | { 15 | // image isn't getting loaded 16 | // ? m('img.defaultAvatar', { 17 | // src: '../data/user.png' 18 | // }) 19 | }, 20 | m('p', v.attrs.firstLetter) 21 | ) 22 | : m('img.avatar', { 23 | src: 'data:image/png;base64,' + imageURI.mData.base64, 24 | }); 25 | }, 26 | }); 27 | 28 | function contactlist(list) { 29 | const result = []; 30 | if (list !== undefined) { 31 | list.map((id) => { 32 | id.isSearched = true; 33 | rs.rsJsonApiRequest('/rsIdentity/isARegularContact', { id: id.mGroupId }, (data) => { 34 | if (data.retval) result.push(id); 35 | console.log(data); 36 | }); 37 | }); 38 | } 39 | return result; 40 | } 41 | 42 | function sortUsers(list) { 43 | if (list !== undefined) { 44 | const result = []; 45 | list.map((id) => { 46 | id.isSearched = true; 47 | result.push(id); 48 | }); 49 | 50 | result.sort((a, b) => a.mGroupName.localeCompare(b.mGroupName)); 51 | return result; 52 | } 53 | return list; 54 | } 55 | 56 | function sortIds(list) { 57 | if (list !== undefined) { 58 | const result = [...list]; 59 | 60 | result.sort((a, b) => rs.userList.username(a).localeCompare(rs.userList.username(b))); 61 | return result; 62 | } 63 | return list; 64 | } 65 | 66 | async function ownIds(consumer = () => {}, onlySigned = false) { 67 | await rs.rsJsonApiRequest('/rsIdentity/getOwnSignedIds', {}, (owns) => { 68 | if (onlySigned) { 69 | consumer(sortIds(owns.ids)); 70 | } else { 71 | rs.rsJsonApiRequest('/rsIdentity/getOwnPseudonimousIds', {}, (pseudo) => { 72 | if (pseudo.ids) consumer(sortIds(pseudo.ids.concat(owns.ids))); 73 | }); 74 | } 75 | }); 76 | } 77 | const SearchBar = () => { 78 | let searchString = ''; 79 | 80 | return { 81 | view: () => 82 | m('input.searchbar', { 83 | type: 'text', 84 | placeholder: 'search', 85 | value: searchString, 86 | oninput: (e) => { 87 | searchString = e.target.value.toLowerCase(); 88 | 89 | rs.userList.users.map((id) => { 90 | if (id.mGroupName.toLowerCase().indexOf(searchString) > -1) { 91 | id.isSearched = true; 92 | } else { 93 | id.isSearched = false; 94 | } 95 | }); 96 | }, 97 | }), 98 | }; 99 | }; 100 | 101 | const regularcontactInfo = () => { 102 | let details = {}; 103 | 104 | return { 105 | oninit: (v) => 106 | rs.rsJsonApiRequest( 107 | '/rsIdentity/getIdDetails', 108 | { 109 | id: v.attrs.id.mGroupId, 110 | }, 111 | (data) => { 112 | details = data.details; 113 | } 114 | ), 115 | view: (v) => 116 | m( 117 | '.identity', 118 | { 119 | key: details.mId, 120 | style: 'display:' + (v.attrs.id.isSearched ? 'block' : 'none'), 121 | }, 122 | [ 123 | m('h4', details.mNickname), 124 | details.mNickname && 125 | m(UserAvatar, { 126 | avatar: details.mAvatar, 127 | firstLetter: details.mNickname.slice(0, 1).toUpperCase(), 128 | }), 129 | m('.details', [ 130 | m('p', 'ID:'), 131 | m('p', details.mId), 132 | m('p', 'Type:'), 133 | m('p', details.mFlags === 14 ? 'Signed ID' : 'Anonymous ID'), 134 | m('p', 'Owner node ID:'), 135 | m('p', details.mPgpId), 136 | m('p', 'Created on:'), 137 | m( 138 | 'p', 139 | typeof details.mPublishTS === 'object' 140 | ? new Date(details.mPublishTS.xint64 * 1000).toLocaleString() 141 | : 'undefiend' 142 | ), 143 | m('p', 'Last used:'), 144 | m( 145 | 'p', 146 | typeof details.mLastUsageTS === 'object' 147 | ? new Date(details.mLastUsageTS.xint64 * 1000).toLocaleDateString() 148 | : 'undefiend' 149 | ), 150 | ]), 151 | m( 152 | 'button', 153 | { 154 | onclick: () => 155 | m.route.set('/chat/:userid/createdistantchat', { 156 | userid: v.attrs.id.mGroupId, 157 | }), 158 | }, 159 | 'Chat' 160 | ), 161 | m('button.red', {}, 'Mail'), 162 | ] 163 | ), 164 | }; 165 | }; 166 | 167 | module.exports = { 168 | sortUsers, 169 | sortIds, 170 | ownIds, 171 | checksudo, 172 | UserAvatar, 173 | contactlist, 174 | SearchBar, 175 | regularcontactInfo, 176 | }; 177 | -------------------------------------------------------------------------------- /webui-src/app/scss/abstracts/_colors.scss: -------------------------------------------------------------------------------- 1 | //---------------------------------- Colors ----------------------------------// 2 | /// Primary Retro color 3 | $primary-retro-color: #118fcc !default; 4 | 5 | /// Primary color 6 | $primary-color: #019dff !default; 7 | 8 | /// Primary light color 9 | $primary-light-color: #9bdaff !default; 10 | 11 | /// Light red 12 | $red-color: #ff3a4a !default; 13 | 14 | /// Dark 15 | $dark-color: #14141b !default; 16 | 17 | /// Light 18 | $light-color: #eef3f6 !default; 19 | 20 | /// Message Starred Color 21 | $golden-yellow-color: #fcba03; 22 | -------------------------------------------------------------------------------- /webui-src/app/scss/abstracts/_functions.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // This file contains all application-wide Sass mixins. 3 | // ----------------------------------------------------------------------------- 4 | 5 | -------------------------------------------------------------------------------- /webui-src/app/scss/abstracts/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'variables'; 2 | @forward 'colors'; 3 | @forward 'mixins'; 4 | @forward 'functions'; 5 | @forward 'typography'; 6 | -------------------------------------------------------------------------------- /webui-src/app/scss/abstracts/_mixins.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // This file contains all application-wide Sass mixins. 3 | // ----------------------------------------------------------------------------- 4 | @use './colors' as *; 5 | 6 | /// Button Mixin 7 | @mixin button($bg-color) { 8 | width: max-content; 9 | height: max-content; 10 | color: white; 11 | background: $bg-color; 12 | font-size: 1rem; 13 | padding: 0.4rem 1rem; 14 | border: 0; 15 | border-radius: 5px; 16 | cursor: pointer; 17 | box-shadow: inset -3px -3px 0 darken($color: $bg-color, $amount: 20); 18 | 19 | &:active { 20 | outline: none; 21 | box-shadow: inset 3px 3px 0 darken($color: $bg-color, $amount: 20); 22 | } 23 | } 24 | 25 | /// Overlay Mixin 26 | @mixin popupOverlay { 27 | position: fixed; 28 | width: 100%; 29 | height: 100%; 30 | top: 0; 31 | left: 0; 32 | z-index: 1; 33 | background-color: rgba(0, 0, 0, 0.2); 34 | } 35 | 36 | /// Blockquote Mixin 37 | @mixin blockquote($type) { 38 | position: relative; 39 | line-height: 1.2; 40 | 41 | @if ($type == 'info') { 42 | color: transparentize($dark-color, 0.2); 43 | border: 1px solid transparentize($primary-retro-color, 0.2); 44 | } @else if ($type == 'warning') { 45 | border: 1px solid transparentize($golden-yellow-color, 0.2); 46 | } @else if ($type == 'danger') { 47 | border: 1px solid transparentize($red-color, 0.2); 48 | } 49 | 50 | &::before { 51 | font-family: 'Font Awesome 5 Free'; 52 | position: absolute; 53 | top: 0.5rem; 54 | left: 0.5rem; 55 | 56 | @if ($type == 'info') { 57 | content: '\f05a'; 58 | color: $primary-color; 59 | } @else if ($type == 'warning') { 60 | content: '\f071'; 61 | color: $golden-yellow-color; 62 | } @else if ($type == 'danger') { 63 | content: '\f05a'; 64 | color: $red-color; 65 | } 66 | } 67 | } 68 | 69 | @mixin flex($direction: '', $justify: '', $align: '', $gap: 0) { 70 | display: flex; 71 | 72 | @if ($direction != '') { 73 | flex-direction: $direction; 74 | } 75 | 76 | @if ($justify != '') { 77 | justify-content: $justify; 78 | } 79 | 80 | @if ($align != '') { 81 | align-items: $align; 82 | } 83 | 84 | @if ($gap !=0) { 85 | gap: $gap; 86 | } 87 | } 88 | 89 | // Font 90 | @mixin fontdef-woff($FontPath, $FontName, $FontVersion: '1.0.0', $FontType: 'Regular') { 91 | src: 92 | url('#{$FontPath}/#{$FontName}-#{$FontType}.woff2') format('woff2'), 93 | url('#{$FontPath}/#{$FontName}-#{$FontType}.woff') format('woff'), 94 | url('#{$FontPath}/#{$FontName}-#{$FontType}.ttf') format('truetype'); 95 | } 96 | -------------------------------------------------------------------------------- /webui-src/app/scss/abstracts/_typography.scss: -------------------------------------------------------------------------------- 1 | //-------------------------------- Typography --------------------------------// 2 | h1 { 3 | font-size: 3rem; 4 | } 5 | 6 | h2 { 7 | font-size: 2.25rem; 8 | } 9 | 10 | h3 { 11 | font-size: 1.875rem; 12 | } 13 | 14 | h4 { 15 | font-size: 1.5rem; 16 | } 17 | 18 | h5 { 19 | font-size: 1.25rem; 20 | } 21 | 22 | h6 { 23 | font-size: 1.125rem; 24 | } 25 | 26 | p { 27 | font-size: 1rem; 28 | } 29 | 30 | .small { 31 | font-size: 0.75rem; 32 | } 33 | 34 | .bold { 35 | font-weight: bold; 36 | } 37 | 38 | h1, 39 | h2, 40 | h3, 41 | h4, 42 | h5, 43 | h6, 44 | p { 45 | font-weight: normal; 46 | } 47 | -------------------------------------------------------------------------------- /webui-src/app/scss/abstracts/_variables.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // This file contains all application-wide Sass variables. 3 | // ----------------------------------------------------------------------------- 4 | $FontPath: './webfonts' !default; 5 | $FontName: 'Roboto' !default; 6 | $FontVersion: '3.008' !default; 7 | -------------------------------------------------------------------------------- /webui-src/app/scss/base/_base.scss: -------------------------------------------------------------------------------- 1 | /****************************** 2 | General site-wide rules 3 | ******************************/ 4 | @use '../abstracts' as *; 5 | 6 | #main { 7 | height: 100vh; 8 | } 9 | 10 | /* Main base div for tabs used by m.route */ 11 | .content { 12 | @include flex; 13 | height: 100%; 14 | overflow: hidden; 15 | } 16 | 17 | /* Main tab content */ 18 | .tab-content { 19 | @include flex; 20 | height: 100%; 21 | width: 100%; 22 | background-color: $light-color; 23 | animation: fadein 0.3s; 24 | overflow: auto; 25 | } 26 | 27 | input[type='text'], 28 | input[type='password'], 29 | input[type='number'], 30 | textarea { 31 | box-sizing: border-box; 32 | background: white; 33 | max-width: 100%; 34 | font-size: 1rem; 35 | font-weight: 400; 36 | border: 1px solid #ccc; 37 | border-radius: 0.25rem; 38 | padding: 0.25rem 0.5rem; 39 | /* Disable chromium's focused element hinting*/ 40 | outline: transparent; 41 | } 42 | input { 43 | &:focus { 44 | border: 1px solid #3ba4d7; 45 | box-shadow: inset 0 0 5px #ccc; 46 | } 47 | &.stretched { 48 | width: 90%; 49 | } 50 | &.small { 51 | max-width: 70%; 52 | padding: 0.1rem; 53 | } 54 | &.searchbar { 55 | width: 40%; 56 | } 57 | } 58 | 59 | a { 60 | cursor: pointer; 61 | &[title='Back'] { 62 | width: max-content; 63 | height: max-content; 64 | padding: 0.475rem 0.75rem; 65 | border-radius: 50%; 66 | transition: 100ms; 67 | &:hover { 68 | background: $light-color; 69 | } 70 | } 71 | } 72 | 73 | table { 74 | padding: 20px; 75 | table-layout: fixed; 76 | width: 100%; 77 | border-collapse: collapse; 78 | text-align: center; 79 | color: #333; 80 | font-size: 1.125rem; 81 | & th { 82 | font-size: 1.125rem; 83 | color: black; 84 | border-bottom: 2px solid #eee; 85 | } 86 | & tr { 87 | border-bottom: 1px solid #eee; 88 | } 89 | } 90 | 91 | h3 { 92 | color: #444; 93 | } 94 | hr { 95 | margin-left: 0; 96 | color: #aaa; 97 | } 98 | 99 | .grid-2col { 100 | display: grid; 101 | grid-template-columns: auto auto; 102 | gap: 1rem; 103 | justify-content: start; 104 | & input[type='checkbox'] { 105 | margin-top: 20px; 106 | } 107 | } 108 | 109 | .error { 110 | color: red; 111 | } 112 | 113 | .tooltip { 114 | color: #333; 115 | position: relative; 116 | display: inline-block; 117 | margin: 0 0.25rem; 118 | } 119 | .tooltiptext { 120 | visibility: hidden; 121 | position: absolute; 122 | top: 100%; 123 | left: 50%; 124 | min-width: 250px; 125 | margin-left: -120px; 126 | z-index: 1; 127 | color: #ccc; 128 | background-color: #333; 129 | font-size: 0.875rem; 130 | text-align: center; 131 | padding: 0.25rem; 132 | border-radius: 0.5rem; 133 | } 134 | .tooltip:hover .tooltiptext { 135 | visibility: visible; 136 | animation: fadein 0.5s; 137 | } 138 | 139 | blockquote { 140 | color: $dark-color; 141 | padding: 0.75rem 1rem 0.75rem 2rem; 142 | border-radius: 0.25rem; 143 | &.info { 144 | @include blockquote('info'); 145 | } 146 | } 147 | 148 | /****************************** 149 | Animations 150 | ******************************/ 151 | 152 | @keyframes fadein { 153 | from { 154 | opacity: 0; 155 | } 156 | to { 157 | opacity: 1; 158 | } 159 | } 160 | .fadein { 161 | animation: fadein 0.5s; 162 | } 163 | 164 | @keyframes swipe-from-left { 165 | from { 166 | margin-left: 100%; 167 | } 168 | to { 169 | margin-left: 0; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /webui-src/app/scss/base/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "reset"; 2 | @forward "base"; 3 | -------------------------------------------------------------------------------- /webui-src/app/scss/base/_reset.scss: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 87.5%; 3 | box-sizing: border-box; 4 | } 5 | 6 | /* Box sizing rules */ 7 | *, 8 | *::before, 9 | *::after { 10 | box-sizing: inherit; 11 | } 12 | 13 | /* Remove default margin */ 14 | body, 15 | h1, 16 | h2, 17 | h3, 18 | h4, 19 | h5, 20 | h6, 21 | p, 22 | figure, 23 | blockquote, 24 | dl, 25 | dd { 26 | margin: 0; 27 | padding: 0; 28 | } 29 | 30 | /* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ 31 | ul[role='list'], 32 | ol[role='list'] { 33 | list-style: none; 34 | } 35 | 36 | /* Set core root defaults */ 37 | html:focus-within { 38 | scroll-behavior: smooth; 39 | } 40 | 41 | /* Set core body defaults */ 42 | body { 43 | text-rendering: optimizeSpeed; 44 | line-height: 1.5; 45 | font-family: 'Roboto', Arial, Helvetica, sans-serif !important; 46 | letter-spacing: -0.025ch; 47 | } 48 | 49 | /* A elements that don't have a class get default styles */ 50 | a:not([class]) { 51 | text-decoration-skip-ink: auto; 52 | } 53 | 54 | /* Make images easier to work with */ 55 | img, 56 | picture { 57 | max-width: 100%; 58 | display: block; 59 | } 60 | 61 | /* Inherit fonts for inputs and buttons */ 62 | input, 63 | button, 64 | textarea, 65 | select { 66 | font: inherit; 67 | } 68 | 69 | /* Remove all animations and transitions for people that prefer not to see them */ 70 | @media (prefers-reduced-motion: reduce) { 71 | html:focus-within { 72 | scroll-behavior: auto; 73 | } 74 | 75 | *, 76 | *::before, 77 | *::after { 78 | animation-duration: 0.01ms !important; 79 | animation-iteration-count: 1 !important; 80 | transition-duration: 0.01ms !important; 81 | scroll-behavior: auto !important; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /webui-src/app/scss/components/_buttons.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // This file contains all application-wide button styles. 3 | // ----------------------------------------------------------------------------- 4 | @use "../abstracts" as *; 5 | 6 | button { 7 | @include button($primary-color); 8 | } 9 | button.red { 10 | @include button($red-color); 11 | } 12 | 13 | -------------------------------------------------------------------------------- /webui-src/app/scss/components/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'buttons'; 2 | @forward 'media'; 3 | @forward 'navbar'; 4 | @forward 'posts'; 5 | @forward 'progress-bar'; 6 | -------------------------------------------------------------------------------- /webui-src/app/scss/components/_media.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts' as *; 2 | 3 | .media-item { 4 | display: flex; 5 | margin-top: 0.5rem; 6 | padding: 1rem; 7 | border: 1px solid transparentize($dark-color, 0.9); 8 | border-radius: 4px; 9 | &__details { 10 | flex-basis: 40%; 11 | @include flex($align: start, $gap: 0.5rem); 12 | & img { 13 | width: 6rem; 14 | object-fit: contain; 15 | } 16 | } 17 | &__desc { 18 | flex-basis: 60%; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /webui-src/app/scss/components/_navbar.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts' as *; 2 | 3 | /* Navbar */ 4 | .nav-menu { 5 | background-color: $dark-color; 6 | box-shadow: 0 5px 5px #222; 7 | @include flex(column, $align: center); 8 | height: 100%; 9 | padding: 0.5rem 0.25rem; 10 | margin-right: 0rem; 11 | 12 | &__logo { 13 | padding: 1.2rem 0; 14 | @include flex($align: center, $gap: 0.3rem); 15 | 16 | img { 17 | width: 1.6rem; 18 | } 19 | 20 | h5 { 21 | line-height: 1; 22 | color: white; 23 | } 24 | } 25 | 26 | &__box { 27 | padding: 2rem 0.125rem; 28 | @include flex(column, $gap: 0.5rem); 29 | position: relative; 30 | 31 | .item { 32 | margin: 0; 33 | padding: 0.675rem 0.5rem; 34 | width: 10rem; 35 | @include flex($align: center); 36 | line-height: 1; 37 | border-radius: 0.5rem; 38 | text-decoration: none; 39 | color: #ccc; 40 | text-transform: capitalize; 41 | transition: 0ms; 42 | 43 | &:hover { 44 | background-color: transparentize($light-color, 0.85); 45 | } 46 | 47 | i.sidenav-icon { 48 | width: 2.5rem; 49 | height: 1.4rem; 50 | display: grid; 51 | place-items: center; 52 | } 53 | } 54 | 55 | .item.item-selected { 56 | color: $primary-light-color; 57 | background-color: transparentize($primary-light-color, 0.85); 58 | font-weight: medium; 59 | } 60 | 61 | button.toggle-nav { 62 | display: none; 63 | position: absolute; 64 | padding: 0; 65 | top: 0; 66 | right: -1rem; 67 | background: lighten($primary-color, 15%); 68 | width: 1.5rem; 69 | height: 1.5rem; 70 | aspect-ratio: 1; 71 | justify-content: center; 72 | align-items: center; 73 | border-radius: 50%; 74 | box-shadow: none; 75 | } 76 | } 77 | 78 | &.collapsed { 79 | .nav-menu__logo { 80 | & h5 { 81 | display: none; 82 | } 83 | } 84 | 85 | .nav-menu__box { 86 | .item { 87 | padding: 0.675rem 0; 88 | width: 2.5rem; 89 | justify-content: center; 90 | transition: 300ms; 91 | 92 | & p { 93 | display: none; 94 | } 95 | } 96 | } 97 | 98 | button i { 99 | rotate: 180deg; 100 | } 101 | } 102 | 103 | // only show toggle button when sidebar is hovered 104 | &:hover button.toggle-nav { 105 | display: flex; 106 | } 107 | } 108 | 109 | .sidebar { 110 | width: 13rem; 111 | background-color: white; 112 | @include flex(column); 113 | 114 | & a { 115 | text-decoration: none; 116 | text-transform: capitalize; 117 | padding: 1rem; 118 | cursor: pointer; 119 | color: #999; 120 | 121 | &:hover { 122 | color: #222; 123 | } 124 | } 125 | 126 | & .selected-sidebar-link { 127 | font-weight: bold; 128 | color: #222; 129 | border-left: 5px solid #3ba4d7; 130 | animation: expand-left-border 0.1s; 131 | } 132 | } 133 | 134 | .sidebarquickview { 135 | & > h6 { 136 | padding: 0.5rem; 137 | } 138 | 139 | & a { 140 | text-decoration: none; 141 | text-transform: capitalize; 142 | padding: 0.5rem 1rem; 143 | display: block; 144 | color: #999; 145 | 146 | & a:hover { 147 | color: #222; 148 | } 149 | } 150 | 151 | & .selected-sidebarquickview-link { 152 | font-weight: bold; 153 | color: #222; 154 | border-left: 5px solid #3ba4d7; 155 | animation: expand-left-border 0.1s; 156 | } 157 | } 158 | 159 | /* Content adjacent to sidebar */ 160 | .node-panel { 161 | width: 100%; 162 | padding: 0.5rem; 163 | animation: fadein 0.5s; 164 | } 165 | 166 | @keyframes expand-left-border { 167 | from { 168 | border-left: 0; 169 | } 170 | 171 | to { 172 | border-left: 5px solid #3ba4d7; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /webui-src/app/scss/components/_posts.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts' as *; 2 | 3 | .posts { 4 | height: 100%; 5 | margin-top: 1rem; 6 | flex-direction: column; 7 | overflow: auto; 8 | &__heading { 9 | @include flex(column, $justify: space-between); 10 | } 11 | &-container { 12 | height: 100%; 13 | padding: 1rem; 14 | display: grid; 15 | grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); 16 | gap: 2rem; 17 | border: 1px solid transparentize($dark-color, 0.9); 18 | border-radius: 4px; 19 | overflow: auto; 20 | &-card { 21 | min-height: 240px; 22 | flex-direction: column; 23 | border: 1px solid transparentize($dark-color, 0.5); 24 | border-radius: 4px; 25 | cursor: pointer; 26 | text-align: center; 27 | & img { 28 | flex-basis: 90%; 29 | object-fit: cover; 30 | } 31 | & p { 32 | padding: 0 0.125rem; 33 | flex-basis: 10%; 34 | overflow: hidden; 35 | text-overflow: ellipsis; 36 | white-space: nowrap; 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /webui-src/app/scss/components/_progress-bar.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts/colors' as *; 2 | 3 | .progress-bar { 4 | width: 100%; 5 | height: 2rem; 6 | position: relative; 7 | text-align: center; 8 | background-color: $light-color; 9 | border-radius: 20px; 10 | overflow: hidden; 11 | &__status { 12 | position: absolute; 13 | top: 0; 14 | left: 0; 15 | height: 100%; 16 | color: $dark-color; 17 | background-color: $primary-color; 18 | } 19 | &__percent { 20 | position: absolute; 21 | inset: 0; 22 | margin: auto; 23 | width: fit-content; 24 | height: fit-content; 25 | } 26 | &-chunks { 27 | position: relative; 28 | margin-top: 0.5rem; 29 | width: 100%; 30 | height: 2rem; 31 | display: flex; 32 | border-radius: 0.25rem; 33 | overflow: hidden; 34 | background-color: $light-color; 35 | & .chunk { 36 | width: 100%; 37 | &[data-chunkVal='0'] { 38 | background-color: transparentize($primary-light-color, 0.8); 39 | } 40 | &[data-chunkVal='1'] { 41 | background-color: $red-color; 42 | } 43 | &[data-chunkVal='2'] { 44 | background-color: $primary-color; 45 | } 46 | &[data-chunkVal='3'] { 47 | background-color: $golden-yellow-color; 48 | } 49 | } 50 | &__percent { 51 | position: absolute; 52 | inset: 0; 53 | margin: auto; 54 | width: fit-content; 55 | height: fit-content; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /webui-src/app/scss/fontface/_Bold.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts' as *; 2 | 3 | /* BEGIN Bold */ 4 | @font-face { 5 | font-family: Roboto; 6 | @include fontdef-woff($FontPath, $FontName, $FontVersion, 'Bold'); 7 | font-weight: 700; 8 | font-style: normal; 9 | } 10 | 11 | @font-face { 12 | font-family: Roboto; 13 | @include fontdef-woff($FontPath, $FontName, $FontVersion, 'Bold'); 14 | font-weight: bold; 15 | font-style: normal; 16 | } 17 | 18 | /* END Bold */ 19 | -------------------------------------------------------------------------------- /webui-src/app/scss/fontface/_BoldItalic.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts' as *; 2 | 3 | /* BEGIN Bold Italic */ 4 | @font-face { 5 | font-family: Roboto; 6 | @include fontdef-woff($FontPath, $FontName, $FontVersion, 'BoldItalic'); 7 | font-weight: 700; 8 | font-style: italic; 9 | } 10 | 11 | @font-face { 12 | font-family: Roboto; 13 | @include fontdef-woff($FontPath, $FontName, $FontVersion, 'BoldItalic'); 14 | font-weight: bold; 15 | font-style: italic; 16 | } 17 | 18 | /* END Bold Italic */ 19 | -------------------------------------------------------------------------------- /webui-src/app/scss/fontface/_Italic.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts' as *; 2 | 3 | /* BEGIN Italic */ 4 | @font-face { 5 | font-family: Roboto; 6 | @include fontdef-woff($FontPath, $FontName, $FontVersion, 'Italic'); 7 | font-weight: 400; 8 | font-style: italic; 9 | } 10 | 11 | @font-face { 12 | font-family: Roboto; 13 | @include fontdef-woff($FontPath, $FontName, $FontVersion, 'Italic'); 14 | font-weight: normal; 15 | font-style: italic; 16 | } 17 | 18 | /* END Italic */ 19 | -------------------------------------------------------------------------------- /webui-src/app/scss/fontface/_Light.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts' as *; 2 | 3 | /* BEGIN Light */ 4 | @font-face { 5 | font-family: Roboto; 6 | @include fontdef-woff($FontPath, $FontName, $FontVersion, 'Light'); 7 | font-weight: 300; 8 | font-style: normal; 9 | } 10 | 11 | /* END Light */ 12 | -------------------------------------------------------------------------------- /webui-src/app/scss/fontface/_LightItalic.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts' as *; 2 | 3 | /* BEGIN Light Italic */ 4 | @font-face { 5 | font-family: Roboto; 6 | @include fontdef-woff($FontPath, $FontName, $FontVersion, 'LightItalic'); 7 | font-weight: 300; 8 | font-style: italic; 9 | } 10 | 11 | /* END Light Italic */ 12 | -------------------------------------------------------------------------------- /webui-src/app/scss/fontface/_Medium.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts' as *; 2 | 3 | /* BEGIN Medium */ 4 | @font-face { 5 | font-family: Roboto; 6 | @include fontdef-woff($FontPath, $FontName, $FontVersion, 'Medium'); 7 | font-weight: 500; 8 | font-style: normal; 9 | } 10 | 11 | /* END Medium */ 12 | -------------------------------------------------------------------------------- /webui-src/app/scss/fontface/_MediumItalic.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts' as *; 2 | 3 | /* BEGIN Medium Italic */ 4 | @font-face { 5 | font-family: Roboto; 6 | @include fontdef-woff($FontPath, $FontName, $FontVersion, 'MediumItalic'); 7 | font-weight: 500; 8 | font-style: italic; 9 | } 10 | 11 | /* END Medium Italic */ 12 | -------------------------------------------------------------------------------- /webui-src/app/scss/fontface/_Regular.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts' as *; 2 | 3 | /* BEGIN Regular */ 4 | @font-face { 5 | font-family: Roboto; 6 | @include fontdef-woff($FontPath, $FontName, $FontVersion, 'Regular'); 7 | font-weight: 400; 8 | font-style: normal; 9 | } 10 | 11 | @font-face { 12 | font-family: Roboto; 13 | @include fontdef-woff($FontPath, $FontName, $FontVersion, 'Regular'); 14 | font-weight: normal; 15 | font-style: normal; 16 | } 17 | 18 | /* END Regular */ 19 | -------------------------------------------------------------------------------- /webui-src/app/scss/fontface/_index.scss: -------------------------------------------------------------------------------- 1 | @forward 'Bold'; 2 | @forward 'BoldItalic'; 3 | @forward 'Medium'; 4 | @forward 'MediumItalic'; 5 | @forward 'Regular'; 6 | @forward 'Italic'; 7 | @forward 'Light'; 8 | @forward 'LightItalic'; 9 | -------------------------------------------------------------------------------- /webui-src/app/scss/layouts/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "widget"; 2 | @forward "modal-container"; 3 | @forward "notification-container"; 4 | -------------------------------------------------------------------------------- /webui-src/app/scss/layouts/_modal-container.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts/' as *; 2 | 3 | #modal-container { 4 | display: none; 5 | position: fixed; 6 | z-index: 1; 7 | height: 100%; 8 | top: 0; 9 | left: 0; 10 | width: 100%; 11 | background-color: rgba(0, 0, 0, 0.2); 12 | } 13 | 14 | .modal-content { 15 | position: absolute; 16 | color: #555; 17 | width: 40%; 18 | min-height: 10rem; 19 | height: max-content; 20 | padding: 1.5rem; 21 | inset: 0; 22 | margin: auto; 23 | background-color: white; 24 | border-radius: 0.5rem; 25 | animation: fadein 0.5s; 26 | @include flex(column); 27 | & button:last-child { 28 | margin-top: auto; 29 | } 30 | & .close-btn { 31 | position: absolute; 32 | right: 1.5rem; 33 | } 34 | // Remove default widget padding 35 | & .widget { 36 | padding: 0; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /webui-src/app/scss/layouts/_notification-container.scss: -------------------------------------------------------------------------------- 1 | #notification-container { 2 | position: absolute; 3 | bottom: 0; 4 | right: 0; 5 | } 6 | -------------------------------------------------------------------------------- /webui-src/app/scss/layouts/_widget.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts/' as *; 2 | 3 | .widget { 4 | height: 100%; 5 | padding: 1rem; 6 | @include flex(column, $gap: 0.5rem); 7 | background-color: white; 8 | border-radius: 0.5rem; 9 | overflow: auto; 10 | & .top-heading { 11 | @include flex($justify: space-between); 12 | } 13 | &__heading { 14 | @include flex($justify: space-between, $align: center); 15 | border-bottom: 2px solid #999; 16 | } 17 | &__body { 18 | height: 100%; 19 | @include flex(column); 20 | overflow: auto; 21 | &-heading { 22 | @include flex($justify: space-between, $align: center); 23 | & .action { 24 | @include flex($gap: 0.5rem); 25 | } 26 | } 27 | &-content { 28 | height: 100%; 29 | overflow: auto; 30 | } 31 | &-box { 32 | @include flex(column, $gap: 0.5rem); 33 | } 34 | } 35 | &-half { 36 | max-width: 50%; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /webui-src/app/scss/main.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | // 1. Configuration and helpers 4 | @use 'abstracts'; 5 | 6 | // 2. Font faces 7 | @use 'fontface'; 8 | 9 | // 2. Vendors specific files 10 | @use 'vendors'; 11 | 12 | // 3. Base stuff 13 | @use 'base'; 14 | 15 | // 4. Components 16 | @use 'components'; 17 | 18 | // 5. Layout-related sections 19 | @use 'layouts'; 20 | 21 | // 6. Page-specific styles 22 | @use 'pages'; 23 | -------------------------------------------------------------------------------- /webui-src/app/scss/pages/_board.scss: -------------------------------------------------------------------------------- 1 | /* subject */ 2 | table.boards th:nth-child(1) { 3 | width: 50%; 4 | text-align: start; 5 | } 6 | /* subject */ 7 | table.boards td:nth-child(1) { 8 | text-align: start; 9 | /* Truncate text with '...' */ 10 | white-space: nowrap; 11 | overflow: hidden; 12 | text-overflow: ellipsis; 13 | } 14 | 15 | table.boards tr:hover { 16 | background-color: #eef3f6; 17 | cursor: pointer; 18 | } 19 | 20 | table.boards tr.hidden { 21 | display: none; 22 | } 23 | 24 | #toggleunsub { 25 | position: relative; 26 | background: gray; 27 | } 28 | 29 | #options { 30 | width: 100px; 31 | text-align: center; 32 | font-size: medium; 33 | margin-left: 20px; 34 | height: 40px; 35 | } 36 | 37 | #composepopup { 38 | height: 80%; 39 | width: 70%; 40 | } 41 | 42 | #mtags { 43 | width: 160px; 44 | text-align: center; 45 | font-size: medium; 46 | margin-left: 10px; 47 | height: 40px; 48 | } 49 | -------------------------------------------------------------------------------- /webui-src/app/scss/pages/_channel.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts' as *; 2 | 3 | .file-section { 4 | margin-top: 2rem; 5 | @include flex(column); 6 | } 7 | 8 | .comments-section { 9 | margin-top: 2rem; 10 | @include flex($justify: space-between); 11 | &__menu { 12 | @include flex($gap: 1rem); 13 | &-id { 14 | @include flex($gap: 0.25rem, $align: center); 15 | } 16 | } 17 | } 18 | 19 | #toggleunsub { 20 | position: relative; 21 | background: gray; 22 | } 23 | 24 | /* subject */ 25 | table.channels { 26 | & th:nth-child(1) { 27 | width: 50%; 28 | text-align: start; 29 | } 30 | /* subject */ 31 | & td:nth-child(1) { 32 | text-align: start; 33 | /* Truncate text with '...' */ 34 | white-space: nowrap; 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | & tr:hover { 39 | background-color: $light-color; 40 | cursor: pointer; 41 | } 42 | & tr.hidden { 43 | display: none; 44 | } 45 | } 46 | 47 | /* subject */ 48 | table { 49 | padding: 0.5rem; 50 | &.comments { 51 | border: 1px solid #eee; 52 | & th { 53 | height: 40px; 54 | &:nth-child(1) { 55 | width: 2%; 56 | } 57 | &:nth-child(2) { 58 | width: 40%; 59 | } 60 | } 61 | & td { 62 | word-wrap: break-word; 63 | &:nth-child(2) { 64 | text-align: start; 65 | } 66 | } 67 | } 68 | &.files { 69 | & th:first-child { 70 | text-align: start; 71 | width: 60%; 72 | } 73 | & tr td:first-child { 74 | text-align: start; 75 | } 76 | & td { 77 | word-wrap: break-word; 78 | } 79 | } 80 | } 81 | /* #options{ 82 | width: 100px; 83 | text-align: center; 84 | font-size: medium; 85 | margin-left: 20px; 86 | height: 40px; 87 | } */ 88 | #mtags { 89 | width: 160px; 90 | text-align: center; 91 | font-size: medium; 92 | margin-left: 10px; 93 | height: 40px; 94 | } 95 | -------------------------------------------------------------------------------- /webui-src/app/scss/pages/_chat.scss: -------------------------------------------------------------------------------- 1 | .lobby { 2 | margin: 10px; 3 | border: 1px solid #aaa; 4 | border-radius: 20px; 5 | } 6 | 7 | .lobby .mainname { 8 | margin: 20px; 9 | font-weight: 100; 10 | font-size: 1.2em; 11 | } 12 | 13 | .topic { 14 | color: #666; 15 | } 16 | 17 | .lobby > .topic { 18 | font-size: 0.95em; 19 | margin-left: 25px; 20 | margin-bottom: 5px; 21 | } 22 | 23 | .lefttitle { 24 | margin-top: 15px; 25 | margin-bottom: 0; 26 | font-weight: 100; 27 | font-size: 1.2em; 28 | } 29 | 30 | .leftname { 31 | margin-top: 5px; 32 | margin-bottom: 5px; 33 | padding: 5px; 34 | font-weight: 100; 35 | font-size: 1em; 36 | } 37 | 38 | .leftlobby > .topic { 39 | font-size: 0.75em; 40 | margin-left: 15px; 41 | margin-bottom: 5px; 42 | } 43 | 44 | .subscribed, 45 | .public { 46 | cursor: pointer; 47 | } 48 | 49 | .leftlobby { 50 | border: 1px solid #aaa; 51 | border-radius: 10px; 52 | margin-top: 5px; 53 | background-color: white; 54 | } 55 | 56 | .leftlobby.selected-lobby, 57 | .selectedidentity { 58 | color: white; 59 | background-color: #3ba4d7; 60 | } 61 | 62 | .rightbar { 63 | position: absolute; 64 | width: 185px; 65 | background-color: white; 66 | overflow: auto; 67 | top: 130px; 68 | bottom: 15px; 69 | right: 15px; 70 | } 71 | 72 | .user { 73 | padding: 5px; 74 | } 75 | 76 | .lobbyName { 77 | padding: 15px; 78 | margin-top: 2rem; 79 | } 80 | 81 | .lobbies { 82 | position: absolute; 83 | width: 185px; 84 | left: 165px; 85 | bottom: 15px; 86 | top: 130px; 87 | overflow: auto; 88 | } 89 | 90 | .messages, 91 | .setup { 92 | position: absolute; 93 | background-color: white; 94 | top: 130px; 95 | left: 360px; 96 | right: 215px; 97 | overflow: auto; 98 | } 99 | 100 | .messages { 101 | bottom: 115px; 102 | } 103 | 104 | .messagetext { 105 | white-space: break-spaces; 106 | margin-right: 5px; 107 | } 108 | 109 | .message > * { 110 | margin-left: 5px; 111 | } 112 | 113 | .username { 114 | color: darkgreen; 115 | font-weight: bolder; 116 | } 117 | 118 | .chatMessage { 119 | position: absolute; 120 | background-color: white; 121 | height: 85px; 122 | bottom: 15px; 123 | right: 215px; 124 | left: 360px; 125 | } 126 | 127 | textarea.chatMsg { 128 | height: 100%; 129 | width: 100%; 130 | } 131 | 132 | .chatatchar { 133 | margin-left: 0.2em; 134 | margin-right: 0.2em; 135 | color: silver; 136 | } 137 | 138 | .setupicon { 139 | margin-left: 1em; 140 | cursor: pointer; 141 | } 142 | 143 | .leaveicon { 144 | margin-left: 1em; 145 | cursor: pointer; 146 | color: #d40000; 147 | } 148 | 149 | .selectidentity { 150 | margin: 15px; 151 | font-size: 1.2em; 152 | } 153 | 154 | .setup > .identity { 155 | cursor: pointer; 156 | } 157 | 158 | .setup { 159 | bottom: 15px; 160 | } 161 | 162 | .createDistantChat { 163 | margin-top: 1em; 164 | } 165 | -------------------------------------------------------------------------------- /webui-src/app/scss/pages/_config.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts' as *; 2 | 3 | .mail { 4 | .permission-flag { 5 | margin-bottom: 1rem; 6 | @include flex($gap: 1rem); 7 | } 8 | &-tags { 9 | padding: 0.5rem; 10 | border: 1px solid transparentize($dark-color, 0.8); 11 | border-radius: 6px; 12 | &__container { 13 | @include flex(column); 14 | & .tag-item { 15 | @include flex($align: center, $gap: 4px); 16 | border-bottom: 1px solid transparentize($dark-color, 0.9); 17 | padding: 2px 0; 18 | 19 | &:last-child { 20 | border: none; 21 | } 22 | &__color { 23 | width: 1.25rem; 24 | height: 1.25rem; 25 | aspect-ratio: 1; 26 | } 27 | &__name { 28 | font-size: 1.125rem; 29 | } 30 | &__modify { 31 | margin-left: auto; 32 | font-size: 0.75rem; 33 | @include flex($gap: 4px); 34 | } 35 | &:hover { 36 | background-color: $light-color; 37 | } 38 | & button, 39 | & button.red { 40 | padding: 0.25rem 0.6rem; 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | .mail-tags-form .input-field { 48 | margin-bottom: 0.5rem; 49 | label { 50 | margin-right: 0.5rem; 51 | } 52 | } 53 | 54 | .external-address { 55 | margin: 0; 56 | padding-left: 1rem; 57 | height: 100px; 58 | overflow: hidden auto; 59 | &::-webkit-scrollbar { 60 | display: none; 61 | } 62 | } 63 | 64 | .proxy-server { 65 | @include flex(column, $gap: 4px); 66 | &__tor, 67 | &__i2p { 68 | & > h4 { 69 | margin-bottom: 0.25rem; 70 | } 71 | & > input { 72 | margin-right: 0.5rem; 73 | } 74 | .proxy-outgoing { 75 | display: inline-flex; 76 | align-items: center; 77 | gap: 0.5rem; 78 | &__status { 79 | width: 1rem; 80 | height: 1rem; 81 | aspect-ratio: 1; 82 | border: 1px solid black; 83 | border-radius: 50%; 84 | } 85 | } 86 | } 87 | } 88 | 89 | .config-files { 90 | @include flex(column, $gap: 1rem); 91 | } 92 | -------------------------------------------------------------------------------- /webui-src/app/scss/pages/_files.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts/' as *; 2 | 3 | .file-view { 4 | width: 100%; 5 | padding: 1rem; 6 | margin-top: 1.5rem; 7 | border-radius: 8px; 8 | border: 1px solid #ccc; 9 | animation: fadein 0.5s; 10 | &__heading { 11 | @include flex($justify: space-between); 12 | margin-bottom: 0.5rem; 13 | &-chunk { 14 | @include flex($gap: 1rem); 15 | } 16 | } 17 | &__body { 18 | @include flex(column, $gap: 1rem); 19 | &-details { 20 | @include flex($align: center); 21 | &-stat { 22 | width: 100%; 23 | display: grid; 24 | grid-template-columns: repeat(5, 1fr); 25 | & span > i { 26 | margin-right: 0.5rem; 27 | } 28 | } 29 | &-action { 30 | @include flex($gap: 1rem); 31 | height: 100%; 32 | & button, 33 | & button.red { 34 | padding: 0.25rem 0.75rem; 35 | } 36 | } 37 | } 38 | } 39 | } 40 | 41 | table.myfiles td { 42 | word-wrap: break-word; 43 | } 44 | table.myfiles th:nth-child(1) { 45 | width: 2%; 46 | } 47 | table.myfiles th:nth-child(2) { 48 | width: 50%; 49 | } 50 | table.myfiles td:nth-child(2) { 51 | text-align: start; 52 | } 53 | table.friendsfiles td { 54 | word-wrap: break-word; 55 | } 56 | table.friendsfiles th:nth-child(1) { 57 | width: 2%; 58 | } 59 | table.friendsfiles th:nth-child(2) { 60 | width: 50%; 61 | } 62 | table.friendsfiles th:nth-child(4) { 63 | width: 40%; 64 | } 65 | table.friendsfiles td:nth-child(2) { 66 | text-align: start; 67 | } 68 | 69 | // File Search 70 | .file-search-container { 71 | margin-top: 1rem; 72 | padding: 8px; 73 | @include flex($gap: 8px); 74 | border: 1px solid transparentize($dark-color, 0.8); 75 | border-radius: 6px; 76 | height: 100%; 77 | overflow: auto; 78 | 79 | &__keywords { 80 | flex-basis: 15%; 81 | padding-right: 0.25rem; 82 | border-right: 1px solid transparentize($dark-color, 0.9); 83 | 84 | & .keywords-container { 85 | @include flex(column); 86 | border-top: 2.5px solid transparentize($dark-color, 0.92); 87 | margin-top: 0.125rem; 88 | padding-top: 0.25rem; 89 | 90 | & a { 91 | font-size: 1.2rem; 92 | text-decoration: none; 93 | color: $dark-color; 94 | 95 | &.selected { 96 | color: $primary-color; 97 | } 98 | } 99 | } 100 | } 101 | 102 | &__results { 103 | flex-basis: 85%; 104 | height: 100%; 105 | overflow: auto; 106 | 107 | & .results-container { 108 | & .results-header { 109 | & tr { 110 | display: flex; 111 | & th { 112 | font-size: 1.25rem; 113 | font-weight: bold; 114 | text-align: left; 115 | 116 | &:nth-child(1) { 117 | flex-basis: 40%; 118 | } 119 | &:nth-child(2) { 120 | flex-basis: 10%; 121 | text-align: center; 122 | } 123 | &:nth-child(3) { 124 | flex-basis: 40%; 125 | } 126 | &:nth-child(4) { 127 | flex-basis: 10%; 128 | } 129 | } 130 | } 131 | } 132 | & .results { 133 | height: 100%; 134 | overflow: auto; 135 | & tr { 136 | display: flex; 137 | & .results { 138 | &__hash, 139 | &__name { 140 | text-align: left; 141 | flex-basis: 40%; 142 | overflow: hidden; 143 | white-space: nowrap; 144 | text-overflow: ellipsis; 145 | 146 | span { 147 | margin-left: 8px; 148 | } 149 | } 150 | &__size { 151 | flex-basis: 10%; 152 | } 153 | &__download { 154 | flex-basis: 10%; 155 | @include flex($justify: start, $align: center); 156 | } 157 | } 158 | } 159 | } 160 | } 161 | } 162 | } 163 | 164 | .search-form { 165 | display: flex; 166 | width: 40%; 167 | input { 168 | width: 100%; 169 | } 170 | button { 171 | margin-left: 0.5rem; 172 | } 173 | } 174 | 175 | .shareManagerPopupOverlay { 176 | @include popupOverlay; 177 | .shareManagerPopup { 178 | position: absolute; 179 | inset: 0; 180 | margin: auto; 181 | width: 80%; 182 | height: 90%; 183 | & > .widget { 184 | padding: 1.5rem; 185 | } 186 | & .close-btn { 187 | position: absolute; 188 | top: 1.5rem; 189 | right: 1.5rem; 190 | } 191 | } 192 | } 193 | 194 | .share-manager { 195 | @include flex(column, $justify: space-between); 196 | &__table { 197 | margin: 1rem 0 auto; 198 | thead { 199 | font-weight: bold; 200 | text-align: left; 201 | td:nth-child(1), 202 | td:nth-child(2) { 203 | padding-left: 0.5rem; 204 | } 205 | td:nth-child(3), 206 | td:nth-child(4) { 207 | & .tooltip { 208 | font-weight: normal; 209 | font-size: 1rem; 210 | } 211 | } 212 | } 213 | tbody { 214 | text-align: left; 215 | td:nth-child(4) { 216 | font-size: 1rem; 217 | } 218 | } 219 | td { 220 | input { 221 | border: 0 !important; 222 | &[type='text'] { 223 | width: 100%; 224 | } 225 | } 226 | &:nth-child(1) { 227 | width: 45%; 228 | } 229 | &:nth-child(2) { 230 | width: 20%; 231 | } 232 | &:nth-child(3) { 233 | width: 10%; 234 | } 235 | &:nth-child(4) { 236 | width: 25%; 237 | } 238 | } 239 | } 240 | &__actions { 241 | @include flex($justify: space-between); 242 | } 243 | &__form { 244 | @include flex(column, $gap: 0.5rem); 245 | &_input { 246 | @include flex(column, $gap: 0.5rem); 247 | input { 248 | flex-grow: 1; 249 | } 250 | } 251 | } 252 | 253 | .share-flags { 254 | /* hide checkbox */ 255 | input.share-flags-check { 256 | display: none; 257 | /* use label with 'for' to manipulate checkbox */ 258 | & + label.share-flags-label { 259 | color: grey; 260 | margin-right: 0.25rem; 261 | padding: 0.25rem 0.25rem 0.125rem; 262 | border: 1px solid #6d6d6d; 263 | border-radius: 0.5rem; 264 | } 265 | &:checked + label.share-flags-label { 266 | color: $primary-retro-color; 267 | } 268 | } 269 | } 270 | label span { 271 | display: inline-block; 272 | width: 1.125rem; 273 | } 274 | } 275 | .manage-visibility { 276 | label { 277 | width: 100%; 278 | cursor: pointer; 279 | } 280 | @include flex($justify: space-between); 281 | } 282 | -------------------------------------------------------------------------------- /webui-src/app/scss/pages/_forums.scss: -------------------------------------------------------------------------------- 1 | .forums-node-panel { 2 | position: relative; 3 | bottom: 200px; 4 | margin-left: 200px; 5 | animation: fadein 0.5s; 6 | } 7 | 8 | /* subject */ 9 | table.forums th:nth-child(1) { 10 | width: 50%; 11 | text-align: start; 12 | } 13 | /* subject */ 14 | table.forums td:nth-child(1) { 15 | text-align: start; 16 | /* Truncate text with '...' */ 17 | white-space: nowrap; 18 | overflow: hidden; 19 | text-overflow: ellipsis; 20 | } 21 | 22 | table.forums tr:hover { 23 | background-color: #eef3f6; 24 | cursor: pointer; 25 | } 26 | 27 | table.forums tr.hidden { 28 | display: none; 29 | } 30 | 31 | #searchforum { 32 | position: relative; 33 | margin-left: 250px; 34 | /* top: 50px; */ 35 | } 36 | 37 | #forumdetails { 38 | position: relative; 39 | padding: 10px; 40 | } 41 | .p { 42 | margin: 0; 43 | } 44 | #toggleunsub { 45 | position: relative; 46 | background: gray; 47 | } 48 | 49 | table.threads tr:hover { 50 | background-color: #eef3f6; 51 | cursor: pointer; 52 | } 53 | table.threads td { 54 | word-wrap: break-word; 55 | } 56 | table.threadreply th:nth-child(2) { 57 | width: 50%; 58 | } 59 | table.threadreply th:nth-child(1) { 60 | width: 2%; 61 | } 62 | table.threadreply td:nth-child(2) { 63 | width: 50%; 64 | text-align: start; 65 | } 66 | table.threadreply td { 67 | word-wrap: break-word; 68 | } 69 | table.threadreply tr:hover { 70 | background-color: #eef3f6; 71 | cursor: pointer; 72 | } 73 | -------------------------------------------------------------------------------- /webui-src/app/scss/pages/_home.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts' as *; 2 | 3 | .homepage { 4 | margin: 2rem auto 0; 5 | @include flex(column, $gap: 4rem); 6 | 7 | .logo { 8 | @include flex($justify: center, $align: center); 9 | & img { 10 | width: 90px; 11 | } 12 | .retroshareText { 13 | @include flex(column, $align: center); 14 | & .retrotext { 15 | font-size: 36px; 16 | font-weight: 600; 17 | line-height: 1.125; 18 | & > span { 19 | color: $primary-retro-color; 20 | } 21 | } 22 | & > b { 23 | font-size: 14px; 24 | line-height: 1; 25 | } 26 | } 27 | } 28 | 29 | .certificate { 30 | @include flex(column, $gap: 4rem); 31 | &__heading { 32 | text-align: center; 33 | & > h1 { 34 | margin-bottom: 1rem; 35 | } 36 | } 37 | &__content { 38 | @include flex(column, $gap: 2rem); 39 | padding: 2rem; 40 | text-align: center; 41 | border: 1.5px solid transparentize($primary-retro-color, 0.8); 42 | border-radius: 6px; 43 | box-shadow: 0px 0px 8px 2px transparentize($dark-color, 0.95); 44 | .rsId > p { 45 | margin-bottom: 0.5rem; 46 | color: $primary-retro-color; 47 | } 48 | .retroshareID { 49 | padding: 0.25rem; 50 | @include flex($align: center); 51 | justify-self: start; 52 | font-size: 1.25rem; 53 | border-radius: 4px; 54 | background: transparentize($dark-color, 0.95); 55 | & .textArea { 56 | padding: 0; 57 | width: 100%; 58 | min-height: 75px; 59 | font-size: 1rem; 60 | font-family: monospace; 61 | background: transparent; 62 | border: none; 63 | resize: none; 64 | } 65 | & i { 66 | color: $primary-retro-color; 67 | } 68 | & > i { 69 | margin: 0 0.5rem; 70 | cursor: pointer; 71 | } 72 | } 73 | 74 | .webhelp { 75 | padding: 0.5rem; 76 | background: whitesmoke; 77 | @include flex($justify: center, $align: center, $gap: 0.5rem); 78 | border-radius: 4px; 79 | border: 1px solid transparentize($dark-color, 0.5); 80 | width: fit-content; 81 | cursor: pointer; 82 | &-container { 83 | display: grid; 84 | place-items: center; 85 | } 86 | &:hover { 87 | background: $light-color; 88 | border: 1px solid $dark-color; 89 | } 90 | & > i { 91 | font-size: 1.2rem; 92 | color: green; 93 | } 94 | } 95 | .add-friend > h6, 96 | .webhelp-container > h6 { 97 | font-weight: normal; 98 | margin-bottom: 0.5rem; 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /webui-src/app/scss/pages/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "login"; 2 | @forward "home"; 3 | @forward "network"; 4 | @forward "people"; 5 | @forward "chat"; 6 | @forward "mail"; 7 | @forward "files"; 8 | @forward "channel"; 9 | @forward "forums"; 10 | @forward "board"; 11 | @forward "config"; 12 | -------------------------------------------------------------------------------- /webui-src/app/scss/pages/_login.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts' as *; 2 | 3 | .login-page { 4 | background-image: linear-gradient( 5 | -45deg, 6 | transparentize($primary-color, 0.25), 7 | transparentize($primary-retro-color, 0.25) 8 | ); 9 | height: 100%; 10 | animation: fadein 0.5s; 11 | .login-container { 12 | background-color: white; 13 | box-shadow: 3px 3px 5px transparentize($dark-color, 0.6); 14 | margin: auto; 15 | position: relative; 16 | top: 100px; 17 | max-width: 400px; 18 | max-height: 500px; 19 | border-radius: 5px; 20 | @include flex(column, $align: center); 21 | 22 | & input { 23 | padding: 0.375rem 0.75rem; 24 | border-radius: 0.275rem; 25 | } 26 | 27 | & * { 28 | margin-bottom: 1rem; 29 | } 30 | & > img { 31 | margin: 1rem 0 2rem; 32 | } 33 | & extra { 34 | margin: 0; 35 | } 36 | & > a { 37 | text-decoration: underline; 38 | cursor: pointer; 39 | } 40 | } 41 | 42 | .extra > label, 43 | .extra > br, 44 | .extra > input { 45 | margin-bottom: 0; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /webui-src/app/scss/pages/_network.scss: -------------------------------------------------------------------------------- 1 | @use '../abstracts' as *; 2 | 3 | .friend { 4 | color: #444; 5 | font-size: 1.2em; 6 | margin: 1rem 0.5rem; 7 | padding: 1.5rem; 8 | border: 1px solid #aaa; 9 | border-radius: 20px; 10 | & i { 11 | float: left; 12 | padding: 0 10px; 13 | cursor: pointer; 14 | } 15 | & h4 { 16 | margin-bottom: 5px; 17 | } 18 | & button { 19 | font-size: 0.9em; 20 | } 21 | &.hidden { 22 | display: none; 23 | } 24 | & .brief-info.online { 25 | color: green; 26 | } 27 | 28 | & .location { 29 | margin: 5px; 30 | border-top: 1px solid #bbb; 31 | display: grid; 32 | grid-template-columns: auto auto; 33 | justify-content: start; 34 | } 35 | & .brief-info { 36 | @include flex($align: center); 37 | justify-self: start; 38 | } 39 | 40 | & .fa-times-circle { 41 | color: #555; 42 | } 43 | & .fa-check-circle { 44 | color: green; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /webui-src/app/scss/pages/_people.scss: -------------------------------------------------------------------------------- 1 | .identity { 2 | color: #444; 3 | font-size: 1.1em; 4 | margin: 20px; 5 | padding: 10px; 6 | border: 1px solid #aaa; 7 | border-radius: 20px; 8 | & > h4 { 9 | margin: 5px; 10 | font-size: 1.3em; 11 | } 12 | & button { 13 | font-size: 0.9em; 14 | } 15 | & .details { 16 | display: grid; 17 | grid-template-columns: 140px auto; 18 | grid-row-gap: 5px; 19 | justify-content: left; 20 | } 21 | } 22 | 23 | .defaultAvatar { 24 | width: 3rem; 25 | height: 3rem; 26 | aspect-ratio: 1; 27 | background: lightsteelblue; 28 | border-radius: 50%; 29 | display: grid; 30 | place-items: center; 31 | & p { 32 | font-weight: 900; 33 | color: #666f7f; 34 | transform: translateY(1px); 35 | } 36 | } 37 | img.avatar { 38 | display: block; 39 | width: 3rem; 40 | height: max-content; 41 | aspect-ratio: 1; 42 | margin-right: 0.3em; 43 | border-radius: 50%; 44 | } 45 | 46 | .counter { 47 | margin-left: 0.5em; 48 | &:before { 49 | content: '('; 50 | } 51 | &:after { 52 | content: ')'; 53 | } 54 | } 55 | 56 | .chatInit { 57 | margin-left: 0.5em; 58 | color: green; 59 | cursor: pointer; 60 | } 61 | -------------------------------------------------------------------------------- /webui-src/app/scss/vendors/_index.scss: -------------------------------------------------------------------------------- 1 | @forward "fontawesome"; 2 | @forward "solid"; 3 | -------------------------------------------------------------------------------- /webui-src/app/scss/vendors/_solid.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome Free 5.9.0 by @fontawesome - https://fontawesome.com 3 | * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) 4 | */ 5 | @font-face { 6 | font-family: 'Font Awesome 5 Free'; 7 | font-style: normal; 8 | font-weight: 900; 9 | font-display: auto; 10 | src: url("./webfonts/fa-solid-900.eot"); 11 | src: url("./webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"), 12 | url("./webfonts/fa-solid-900.woff2") format("woff2"), 13 | url("./webfonts/fa-solid-900.woff") format("woff"), 14 | url("./webfonts/fa-solid-900.ttf") format("truetype"), 15 | url("./webfonts/fa-solid-900.svg#fontawesome") format("svg"); 16 | } 17 | 18 | .fa, 19 | .fas { 20 | font-family: 'Font Awesome 5 Free'; 21 | font-weight: 900; 22 | } 23 | -------------------------------------------------------------------------------- /webui-src/app/widgets.js: -------------------------------------------------------------------------------- 1 | const m = require('mithril'); 2 | const Sidebar = () => { 3 | let active = 0; 4 | return { 5 | view: (v) => 6 | m( 7 | '.sidebar', 8 | v.attrs.tabs.map((panelName, index) => 9 | m( 10 | m.route.Link, 11 | { 12 | class: index === active ? 'selected-sidebar-link' : '', 13 | onclick: () => (active = index), 14 | href: v.attrs.baseRoute + panelName, 15 | }, 16 | panelName 17 | ) 18 | ) 19 | ), 20 | }; 21 | }; 22 | const SidebarQuickView = () => { 23 | // for the Mail tab, to be moved later. 24 | let quickactive = -1; 25 | return { 26 | view: (v) => 27 | m( 28 | '.sidebarquickview', 29 | m('h4', 'Quick View'), 30 | v.attrs.tabs.map((panelName, index) => 31 | m( 32 | m.route.Link, 33 | { 34 | class: index === quickactive ? 'selected-sidebarquickview-link' : '', 35 | onclick: () => (quickactive = index), 36 | href: v.attrs.baseRoute + panelName, 37 | }, 38 | panelName 39 | ) 40 | ) 41 | ), 42 | }; 43 | }; 44 | 45 | // There are ways of doing this inside m.route but it is probably 46 | // cleaner and faster when kept outside of the main auto 47 | // rendering system 48 | function popupMessage(message) { 49 | const container = document.getElementById('modal-container'); 50 | container.style.display = 'block'; 51 | m.render( 52 | container, 53 | m('.modal-content', [ 54 | m( 55 | 'button.red.close-btn', 56 | { 57 | onclick: () => (container.style.display = 'none'), 58 | }, 59 | m('i.fas.fa-times') 60 | ), 61 | message, 62 | ]) 63 | ); 64 | } 65 | 66 | module.exports = { 67 | Sidebar, 68 | SidebarQuickView, 69 | popupMessage, 70 | }; 71 | -------------------------------------------------------------------------------- /webui-src/assets/images/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-Bold.woff -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-Bold.woff2 -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-BoldItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-BoldItalic.woff -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-BoldItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-BoldItalic.woff2 -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-Italic.ttf -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-Italic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-Italic.woff -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-Italic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-Italic.woff2 -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-Light.ttf -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-Light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-Light.woff -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-Light.woff2 -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-LightItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-LightItalic.woff -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-LightItalic.woff2 -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-Medium.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-Medium.woff -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-Medium.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-Medium.woff2 -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-MediumItalic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-MediumItalic.woff -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-MediumItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-MediumItalic.woff2 -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-Regular.woff -------------------------------------------------------------------------------- /webui-src/assets/webfonts/Roboto-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/Roboto-Regular.woff2 -------------------------------------------------------------------------------- /webui-src/assets/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /webui-src/assets/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /webui-src/assets/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /webui-src/assets/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RetroShare/RSNewWebUI/edfc638a6af87d0f60489632416105e1fa13fc4e/webui-src/assets/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /webui-src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | RetroShare 9 | 10 | 11 | 12 | 13 |
14 |

Javascript not found!

15 | 20 | 21 |
22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /webui-src/make-src/.prettierignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /webui-src/make-src/README.md: -------------------------------------------------------------------------------- 1 | **This folder contains files needed to create webfiles at compile-time and qmake** 2 | 3 | * init.sh creates dummy files at qmake 4 | * build.sh creates files at compile time 5 | * template.js start of compiled app.js, containing additional created content 6 | -------------------------------------------------------------------------------- /webui-src/make-src/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM create webfiles from sources at compile time (works without npm/node.js) 3 | 4 | setlocal enabledelayedexpansion 5 | 6 | echo "### Starting WebUI build ###" 7 | 8 | set src=%~dp0..\..\webui-src 9 | 10 | rem Output destination 11 | if "%~1" == "" ( 12 | set publicdest=%~dp0..\..\webui 13 | ) else ( 14 | set publicdest=%~1\webui 15 | ) 16 | 17 | if exist "%publicdest%" echo removing existing %publicdest%&&rd %publicdest% /S /Q 18 | 19 | echo creating %publicdest% 20 | md %publicdest% 21 | 22 | rem Make full path 23 | pushd %publicdest% 24 | set publicdest=%cd% 25 | popd 26 | 27 | echo copying html file 28 | xcopy /s %src%\index.html %publicdest% 29 | 30 | echo copying css file 31 | xcopy /s %src%\styles.css %publicdest% 32 | 33 | echo building app.js 34 | echo - copying template.js ... 35 | copy %src%\make-src\template.js %publicdest%\app.js 36 | 37 | pushd %src%\app 38 | set "basefolder=%cd%\" 39 | for /R %%F in (*.js) do call :addfile-js "%basefolder%" "%%F" 40 | popd 41 | 42 | echo copying assets folder 43 | xcopy /s %src%\assets\ %publicdest% 44 | 45 | echo "### WebUI build complete ###" 46 | 47 | goto :EOF 48 | 49 | :addfile-js 50 | set basefolder=%~1 51 | set fname=%~2 52 | 53 | set registername=%~dpn2 54 | set registername=!registername:%basefolder%=! 55 | set registername=%registername:\=/% 56 | 57 | echo - adding %registername% ... 58 | echo require.register("%registername%", function(exports, require, module) { >> %publicdest%\app.js 59 | type %fname% >> %publicdest%\app.js 60 | echo. >> %publicdest%\app.js 61 | echo }); >> %publicdest%\app.js 62 | 63 | :EOF 64 | -------------------------------------------------------------------------------- /webui-src/make-src/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # create webfiles from sources at compile time (works without npm/node.js) 4 | 5 | echo "### Starting WebUI build ###" 6 | 7 | src=$(readlink -f $(dirname $0))/../../webui-src 8 | 9 | if [ "$1" = "" ]; then 10 | publicdest=$(readlink -f $(dirname $0))/../../webui 11 | else 12 | publicdest=$1/webui 13 | fi 14 | 15 | if [ "$2" = "" ]; then 16 | if [ -d "$publicdest" ]; then 17 | echo removing existing $publicdest 18 | rm $publicdest -R 19 | fi 20 | fi 21 | 22 | if [ ! -d "$publicdest" ]; then 23 | echo creating $publicdest 24 | mkdir $publicdest 25 | fi 26 | 27 | # For using recursive directory search(requires bash v4+) 28 | shopt -s globstar 29 | 30 | if [ "$2" = "" ]||[ "$2" = "index.html" ]; then 31 | echo copying html file 32 | cp $src/index.html $publicdest/ 33 | fi 34 | 35 | if [ "$2" = "" ]||[ "$2" = "styles.css" ]; then 36 | echo copying css file 37 | cp $src/styles.css $publicdest/ 38 | fi 39 | 40 | if [ "$2" = "" ]||[ "$2" = "app.js" ]; then 41 | echo building app.js: 42 | echo - copying template.js ... 43 | cp $src/make-src/template.js $publicdest/app.js 44 | 45 | js_source=$src/app/ 46 | for filename in $src/app/**/*.js; do 47 | fname="${filename#$js_source}" 48 | fname="${fname%.*}" 49 | echo - adding $fname ... 50 | echo require.register\(\"$fname\", function\(exports, require, module\) { >> $publicdest/app.js 51 | cat $filename >> $publicdest/app.js 52 | echo >> $publicdest/app.js 53 | echo }\)\; >> $publicdest/app.js 54 | done 55 | fi 56 | 57 | echo copying assets folder 58 | cp -r $src/assets/* $publicdest/ 59 | 60 | if [ "$2" != "" ]&&[ "$3" != "" ]; then 61 | if [ ! -d "$3/webui" ]; then 62 | echo creating $3/webui 63 | mkdir $3/webui 64 | fi 65 | echo copying $2 nach $3/webui/$2 66 | cp $publicdest/$2 $3/webui/$2 67 | fi 68 | echo "### WebUI build complete ###" 69 | -------------------------------------------------------------------------------- /webui-src/make-src/init.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | REM create dummy webfiles at qmake run 3 | 4 | set publicdest=%1\webui 5 | if "%1" == "" set publicdest=..\..\webui 6 | 7 | if exist %publicdest% echo removing %publicdest%&&rd %publicdest% /S /Q 8 | 9 | echo creating %publicdest% 10 | md %publicdest% 11 | 12 | echo creating %publicdest%\app.js, %publicdest%\styles.css, %publicdest%\index.html 13 | echo. > %publicdest%\app.js 14 | echo. > %publicdest%\styles.css 15 | echo. > %publicdest%\index.html 16 | -------------------------------------------------------------------------------- /webui-src/make-src/init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # create dummy webfiles at qmake run 4 | 5 | if [ "$1" = "" ];then 6 | publicdest=../../webui 7 | else 8 | publicdest=$1/webui 9 | fi 10 | 11 | if [ -d "$publicdest" ]; then 12 | echo removing $publicdest 13 | rm $publicdest -R 14 | fi 15 | 16 | echo creating $publicdest 17 | mkdir $publicdest 18 | 19 | echo creating $publicdest/app.js, $publicdest/styles.css, $publicdest/index.html 20 | touch $publicdest/app.js -d 1970-01-01 21 | touch $publicdest/styles.css -d 1970-01-01 22 | touch $publicdest/index.html -d 1970-01-01 23 | -------------------------------------------------------------------------------- /webui-src/make-src/template.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | const globals = typeof window === 'undefined' ? global : window; 5 | if (typeof globals.require === 'function') return; 6 | 7 | let modules = {}, 8 | cache = {}, 9 | aliases = {}, 10 | has = {}.hasOwnProperty; 11 | 12 | const unalias = function(alias, loaderPath) { 13 | const _cmp = 'components/'; 14 | let start = 0; 15 | if (loaderPath) { 16 | if (loaderPath.startsWith(_cmp)) { 17 | start = _cmp.length; 18 | } 19 | if (loaderPath.indexOf('/', start) > 0) { 20 | loaderPath = loaderPath.substring( 21 | start, 22 | loaderPath.indexOf('/', start) 23 | ); 24 | } 25 | } 26 | const result = 27 | aliases[alias + '/index.js'] || 28 | aliases[loaderPath + '/deps/' + alias + '/index.js']; 29 | if (result) { 30 | return _cmp + result.substring(0, result.length - '.js'.length); 31 | } 32 | return alias; 33 | }; 34 | 35 | const expand = function(root, name) { 36 | const _reg = /^\.\.?(\/|$)/; 37 | let results = [], 38 | parts = (_reg.test(name) ? root + '/' + name : name).split('/'); 39 | for (let part of parts) { 40 | if (part === '..') { 41 | results.pop(); 42 | } else if (part !== '.' && part !== '') { 43 | results.push(part); 44 | } 45 | } 46 | return results.join('/'); 47 | }; 48 | 49 | const dirname = function(path) { 50 | return path 51 | .split('/') 52 | .slice(0, -1) 53 | .join('/'); 54 | }; 55 | 56 | const localRequire = function(path) { 57 | return function(name) { 58 | let absolute = expand(dirname(path), name); 59 | return globals.require(absolute, path); 60 | }; 61 | }; 62 | 63 | const initModule = function(name, definition) { 64 | let module = { id: name, exports: {} }; 65 | cache[name] = module; 66 | definition(module.exports, localRequire(name), module); 67 | return module.exports; 68 | }; 69 | 70 | const require = function(name, loaderPath) { 71 | if (loaderPath === undefined) loaderPath = '/'; 72 | let path = unalias(name, loaderPath); 73 | 74 | if (path in cache) return cache[path].exports; 75 | if (path in modules) return initModule(path, modules[path]); 76 | 77 | let dirIndex = expand(path, './index'); 78 | if (dirIndex in cache) return cache[dirIndex].exports; 79 | if (dirIndex in modules) return initModule(dirIndex, modules[dirIndex]); 80 | 81 | throw new Error( 82 | 'Cannot find module "' + name + '" from ' + '"' + loaderPath + '"' 83 | ); 84 | }; 85 | 86 | require.alias = function(from, to) { 87 | aliases[to] = from; 88 | }; 89 | 90 | require.register = require.define = function(bundle, fn) { 91 | if (typeof bundle === 'object') { 92 | for (let key in bundle) { 93 | if (has.call(bundle, key)) { 94 | modules[key] = bundle[key]; 95 | } 96 | } 97 | } else { 98 | modules[bundle] = fn; 99 | } 100 | }; 101 | 102 | require.list = function() { 103 | let result = []; 104 | for (let item in modules) { 105 | if (has.call(modules, item)) { 106 | result.push(item); 107 | } 108 | } 109 | return result; 110 | }; 111 | 112 | require._cache = cache; 113 | globals.require = require; 114 | })(); 115 | -------------------------------------------------------------------------------- /webui-src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webui-src", 3 | "version": "1.0.0", 4 | "description": "Retroshare's Web Interface", 5 | "scripts": { 6 | "watch": "rm ./styles.css && sass --watch --embed-sources --embed-source-map ./app/scss/main.scss ./styles.css", 7 | "build": "sass --no-source-map --style=compressed ./app/scss/main.scss ./styles.css" 8 | }, 9 | "license": "ISC", 10 | "devDependencies": { 11 | "sass": "^1.60.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /webui-src/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | devDependencies: 4 | sass: 5 | specifier: ^1.60.0 6 | version: 1.60.0 7 | 8 | packages: 9 | 10 | /anymatch@3.1.3: 11 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 12 | engines: {node: '>= 8'} 13 | dependencies: 14 | normalize-path: 3.0.0 15 | picomatch: 2.3.1 16 | dev: true 17 | 18 | /binary-extensions@2.2.0: 19 | resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} 20 | engines: {node: '>=8'} 21 | dev: true 22 | 23 | /braces@3.0.2: 24 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 25 | engines: {node: '>=8'} 26 | dependencies: 27 | fill-range: 7.0.1 28 | dev: true 29 | 30 | /chokidar@3.5.3: 31 | resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} 32 | engines: {node: '>= 8.10.0'} 33 | dependencies: 34 | anymatch: 3.1.3 35 | braces: 3.0.2 36 | glob-parent: 5.1.2 37 | is-binary-path: 2.1.0 38 | is-glob: 4.0.3 39 | normalize-path: 3.0.0 40 | readdirp: 3.6.0 41 | optionalDependencies: 42 | fsevents: 2.3.2 43 | dev: true 44 | 45 | /fill-range@7.0.1: 46 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 47 | engines: {node: '>=8'} 48 | dependencies: 49 | to-regex-range: 5.0.1 50 | dev: true 51 | 52 | /fsevents@2.3.2: 53 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 54 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 55 | os: [darwin] 56 | requiresBuild: true 57 | dev: true 58 | optional: true 59 | 60 | /glob-parent@5.1.2: 61 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 62 | engines: {node: '>= 6'} 63 | dependencies: 64 | is-glob: 4.0.3 65 | dev: true 66 | 67 | /immutable@4.3.0: 68 | resolution: {integrity: sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==} 69 | dev: true 70 | 71 | /is-binary-path@2.1.0: 72 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 73 | engines: {node: '>=8'} 74 | dependencies: 75 | binary-extensions: 2.2.0 76 | dev: true 77 | 78 | /is-extglob@2.1.1: 79 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 80 | engines: {node: '>=0.10.0'} 81 | dev: true 82 | 83 | /is-glob@4.0.3: 84 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 85 | engines: {node: '>=0.10.0'} 86 | dependencies: 87 | is-extglob: 2.1.1 88 | dev: true 89 | 90 | /is-number@7.0.0: 91 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 92 | engines: {node: '>=0.12.0'} 93 | dev: true 94 | 95 | /normalize-path@3.0.0: 96 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 97 | engines: {node: '>=0.10.0'} 98 | dev: true 99 | 100 | /picomatch@2.3.1: 101 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 102 | engines: {node: '>=8.6'} 103 | dev: true 104 | 105 | /readdirp@3.6.0: 106 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 107 | engines: {node: '>=8.10.0'} 108 | dependencies: 109 | picomatch: 2.3.1 110 | dev: true 111 | 112 | /sass@1.60.0: 113 | resolution: {integrity: sha512-updbwW6fNb5gGm8qMXzVO7V4sWf7LMXnMly/JEyfbfERbVH46Fn6q02BX7/eHTdKpE7d+oTkMMQpFWNUMfFbgQ==} 114 | engines: {node: '>=12.0.0'} 115 | hasBin: true 116 | dependencies: 117 | chokidar: 3.5.3 118 | immutable: 4.3.0 119 | source-map-js: 1.0.2 120 | dev: true 121 | 122 | /source-map-js@1.0.2: 123 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 124 | engines: {node: '>=0.10.0'} 125 | dev: true 126 | 127 | /to-regex-range@5.0.1: 128 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 129 | engines: {node: '>=8.0'} 130 | dependencies: 131 | is-number: 7.0.0 132 | dev: true 133 | -------------------------------------------------------------------------------- /webui.pro: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # Copyright (C) 2018, Retroshare team # 3 | # # 4 | # This program is free software: you can redistribute it and/or modify # 5 | # it under the terms of the GNU Affero General Public License as # 6 | # published by the Free Software Foundation, either version 3 of the # 7 | # License, or (at your option) any later version. # 8 | # # 9 | # This program is distributed in the hope that it will be useful, # 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of # 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # 12 | # GNU Affero General Public License for more details. # 13 | # # 14 | # You should have received a copy of the GNU Affero General Public License # 15 | # along with this program. If not, see . # 16 | ################################################################################ 17 | 18 | !include("../retroshare.pri"): warning("Could not include file retroshare.pri") 19 | 20 | TEMPLATE = subdirs 21 | SUBDIRS= # don't build anything 22 | CONFIG -= qt 23 | 24 | WEBUI_SRC_HTML = $$PWD/webui-src/index.html 25 | 26 | WEBUI_SRC_JS = $$PWD/webui-src/app/rswebui.js 27 | WEBUI_SRC_JS += $$PWD/webui-src/app/mithril.js 28 | WEBUI_SRC_JS += $$PWD/webui-src/app/login.js 29 | WEBUI_SRC_JS += $$PWD/webui-src/app/main.js 30 | WEBUI_SRC_JS += $$PWD/webui-src/app/home.js 31 | 32 | WEBUI_SRC_CSS = $$PWD/webui-src/styles.css 33 | 34 | WEBUI_SRC_IMAGE = $$PWD/data/retroshare.svg 35 | 36 | win32-g++ { 37 | isEmpty(QMAKE_SH) { 38 | # Windows native build 39 | WEBUI_SRC_SCRIPT = $$PWD/webui-src/make-src/build.bat 40 | } else { 41 | # MSYS2 build 42 | WEBUI_SRC_SCRIPT = $$PWD/webui-src/make-src/build.sh 43 | } 44 | 45 | OUTPUT_DIR = $$shadowed($$PWD) 46 | OUTPUT_WEBUI_DIR = $${OUTPUT_DIR}/webui 47 | 48 | WEBUI_SRC_HTML += $$WEBUI_SRC_SCRIPT 49 | 50 | # Use all input files as depends 51 | create_webfiles.depends = $$WEBUI_SRC_HTML $$WEBUI_SRC_JS $$WEBUI_SRC_CSS 52 | # Use one generated output file as target 53 | create_webfiles.target = $${OUTPUT_WEBUI_DIR}/app.js 54 | # The batch file creates all output files at once 55 | create_webfiles.commands = $$shell_path($$WEBUI_SRC_SCRIPT) $$shell_path($$OUTPUT_DIR) 56 | 57 | QMAKE_EXTRA_TARGETS += create_webfiles 58 | ALL_DEPS += $${create_webfiles.target} 59 | 60 | # Clean output folder 61 | clean_webfiles.target = clean_webfiles 62 | isEmpty(QMAKE_SH) { 63 | # Windows native build 64 | clean_webfiles.commands = if exist $$system_path($$OUTPUT_WEBUI_DIR) $(DEL_DIR) /S /Q $$system_path($$OUTPUT_WEBUI_DIR) 65 | } else { 66 | # MSYS2 build 67 | clean_webfiles.commands = rm -rf $$system_path($${OUTPUT_WEBUI_DIR}) 68 | } 69 | 70 | QMAKE_EXTRA_TARGETS += clean_webfiles 71 | CLEAN_DEPS += clean_webfiles 72 | } else { 73 | # create dummy files, we need it to include files on first try 74 | 75 | system(webui-src/make-src/build.sh .) 76 | 77 | WEBUI_SRC_SCRIPT = webui-src/make-src/build.sh 78 | 79 | WEBUI_SRC_HTML += $$WEBUI_SRC_SCRIPT 80 | 81 | create_webfiles_html.output = webui/index.html 82 | create_webfiles_html.input = WEBUI_SRC_HTML 83 | create_webfiles_html.commands = sh $$_PRO_FILE_PWD_/webui-src/make-src/build.sh $$_PRO_FILE_PWD_ index.html . 84 | create_webfiles_html.variable_out = JUNK 85 | create_webfiles_html.CONFIG = combine no_link 86 | 87 | create_webfiles_js.output = webui/app.js 88 | create_webfiles_js.input = WEBUI_SRC_JS 89 | create_webfiles_js.commands = sh $$_PRO_FILE_PWD_/webui-src/make-src/build.sh $$_PRO_FILE_PWD_ app.js . 90 | create_webfiles_js.variable_out = JUNK 91 | create_webfiles_js.CONFIG = combine no_link 92 | 93 | create_webfiles_css.output = webui/styles.css 94 | create_webfiles_css.input = WEBUI_SRC_CSS 95 | create_webfiles_css.commands = sh $$_PRO_FILE_PWD_/webui-src/make-src/build.sh $$_PRO_FILE_PWD_ styles.css . 96 | create_webfiles_css.variable_out = JUNK 97 | create_webfiles_css.CONFIG = combine no_link 98 | 99 | QMAKE_EXTRA_COMPILERS += create_webfiles_html create_webfiles_js create_webfiles_css 100 | 101 | webui_base_files.path = "$${RS_DATA_DIR}/webui/" 102 | webui_base_files.files = webui/index.html \ 103 | webui/styles.css \ 104 | webui/app.js 105 | 106 | webui_image_files.path = "$${RS_DATA_DIR}/webui/images/" 107 | webui_image_files.files = webui/images/retroshare.svg 108 | 109 | webui_font_files.path = "$${RS_DATA_DIR}/webui/webfonts/" 110 | webui_font_files.files = webui/webfonts/fa-solid-900.eot \ 111 | webui/webfonts/fa-solid-900.svg \ 112 | webui/webfonts/fa-solid-900.ttf \ 113 | webui/webfonts/fa-solid-900.woff \ 114 | webui/webfonts/fa-solid-900.woff2 115 | 116 | INSTALLS += webui_base_files webui_font_files webui_image_files 117 | } 118 | --------------------------------------------------------------------------------