├── .dockerignore
├── .gitignore
├── .gitlab-ci.yml
├── .htaccess
├── DOCUMENTATION.md
├── Dockerfile
├── LICENSE.lgpl
├── Makefile
├── README.md
├── api
├── backlog
│ └── index.php
├── search
│ └── index.php
└── searchbuffer
│ └── index.php
├── database
├── Config.php
├── Database.php
├── User.php
├── backends
│ ├── Backend.php
│ ├── BackendFactory.php
│ ├── PostgresSmartBackend.php
│ └── SQLiteSmartBackend.php
└── helper
│ ├── AuthHelper.php
│ ├── RendererHelper.php
│ ├── SessionHelper.php
│ ├── TranslationHelper.php
│ └── ViewHelper.php
├── favicon.ico
├── favicon.png
├── favicon.svg
├── index.php
├── login.php
├── package-lock.json
├── package.json
├── qrs_config.default.php
├── res
├── css
│ ├── _animations.sass
│ ├── _content.sass
│ ├── _font.sass
│ ├── _icons.sass
│ ├── _loading.sass
│ ├── _mirccolor.sass
│ ├── _nav.sass
│ ├── _searchoptions.sass
│ ├── _sendercolor.sass
│ ├── _textfield.sass
│ ├── _util.sass
│ ├── login.css
│ ├── login.css.map
│ ├── login.sass
│ ├── search.css
│ ├── search.css.map
│ └── search.sass
├── fonts
│ ├── roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-700.woff
│ ├── roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-700.woff2
│ ├── roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-regular.woff
│ └── roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-regular.woff2
├── icons
│ ├── MaterialIcons-Regular.woff
│ ├── MaterialIcons-Regular.woff2
│ ├── chevron-down.svg
│ ├── chevron-up.svg
│ ├── dots-horizontal.svg
│ └── error.png
└── js
│ ├── component
│ ├── app.js
│ ├── app.jsx
│ ├── buffer.js
│ ├── buffer.jsx
│ ├── context.js
│ ├── context.jsx
│ ├── error.js
│ ├── error.jsx
│ ├── history.js
│ ├── history.jsx
│ ├── historyelement.js
│ ├── historyelement.jsx
│ ├── loading.js
│ ├── loading.jsx
│ ├── loadmore.js
│ ├── loadmore.jsx
│ ├── message.js
│ ├── message.jsx
│ ├── nav.js
│ ├── nav.jsx
│ ├── nohistoryelement.js
│ └── nohistoryelement.jsx
│ └── util
│ ├── component.js
│ ├── highlighthandler.js
│ ├── loader.js
│ ├── mirccolorhandler.js
│ ├── moment-with-locales.js
│ ├── nativejsx-prototypes.js
│ ├── sendercolorhandler.js
│ ├── statehandler.js
│ └── storage.js
├── templates
├── login.phtml
└── search.phtml
└── translations
├── de.json
└── en.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | /LICENSE.*
2 | /package.json
3 | /package-lock.json
4 | /README.md
5 | /qrs_config.php
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /*.iml
3 | /node_modules
4 | /qrs_config.php
5 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | build:
2 | stage: build
3 | image:
4 | name: gcr.io/kaniko-project/executor:debug
5 | entrypoint: [ "" ]
6 | script:
7 | - mkdir -p /kaniko/.docker
8 | - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json
9 | - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA} --destination $CI_REGISTRY_IMAGE:latest
10 |
--------------------------------------------------------------------------------
/.htaccess:
--------------------------------------------------------------------------------
1 |
2 | Order allow,deny
3 | deny from all
4 | Require all denied
5 |
6 |
7 |
8 | RedirectMatch 403 ^.*/\.git.*$
9 |
10 |
--------------------------------------------------------------------------------
/DOCUMENTATION.md:
--------------------------------------------------------------------------------
1 | # Documentation
2 |
3 | ## API Endpoints
4 |
5 | Authentication happens via [HTTP basic auth](https://tools.ietf.org/html/rfc7617)
6 |
7 | ### api/backlog/
8 |
9 | This endpoint returns full backlog for a single quassel buffer, centered around a certain message.
10 |
11 | **Parameters**:
12 |
13 | | Name | Type | Required | Description |
14 | | -------- | ------ | -------- | ----------------------------------------------------------------- |
15 | | `anchor` | Number | `true` | Index message for which to retrieve surrounding backlog |
16 | | `buffer` | Number | `true` | Chat/Buffer from which messages should be retrieved |
17 | | `before` | Number | `true` | Number of messages to load chronologically before the anchorpoint |
18 | | `after` | Number | `true` | Number of messages to load chronologically after the anchorpoint |
19 |
20 | ### api/search/
21 |
22 | This endpoint returns search results across all buffers.
23 |
24 | **Parameters**:
25 |
26 | | Name | Type | Required | Description |
27 | | --------- | --------- | -------- | ------------------------------------------------------------------------------ |
28 | | `query` | String | `true` | Query to filter messages |
29 | | `since` | Timestamp | `false` | If set, only show messages received after this timestamp. Format: '1970-01-01' |
30 | | `before` | Timestamp | `false` | If set, only show messages received beforethis timestamp. Format: '1970-01-01' |
31 | | `buffer` | String | `false` | If set, only show messages if the name of the chat/buffer they are in matches |
32 | | `network` | String | `false` | If set, only show messages if the name of the network they are in matches |
33 | | `sender` | String | `false` | If set, only show messages if the nick!user@host of the sender matches |
34 | | `limit` | Number | `true` | Number of messages to return per buffer |
35 |
36 | ### api/searchbuffer/
37 |
38 | This endpoint returns search results for a single quassel buffer.
39 |
40 | **Parameters**:
41 |
42 | | Name | Type | Required | Description |
43 | | --------- | --------- | -------- | ------------------------------------------------------------------------------ |
44 | | `query` | String | `true` | Query to filter messages |
45 | | `since` | Timestamp | `false` | If set, only show messages received after this timestamp. Format: '1970-01-01' |
46 | | `before` | Timestamp | `false` | If set, only show messages received beforethis timestamp. Format: '1970-01-01' |
47 | | `buffer` | Number | `true` | Id of the buffer to search in |
48 | | `sender` | String | `false` | If set, only show messages if the nick!user@host of the sender matches |
49 | | `offset` | Number | `true` | Number of results to skip (for pagination) |
50 | | `limit` | Number | `true` | Number of messages to return |
51 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM trafex/php-nginx:latest
2 |
3 | USER root
4 |
5 | RUN apk add --no-cache --update \
6 | php82-json \
7 | php82-pdo_sqlite \
8 | php82-pdo_pgsql
9 |
10 | USER nobody
11 |
12 | ADD . /var/www/html/
13 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | IMAGE := k8r.eu/justjanne/$(shell basename $(shell git remote get-url origin) .git)
2 | TAGS := $(shell git describe --always --tags HEAD)
3 |
4 | .PHONY: build
5 | build:
6 | docker build --pull -t $(IMAGE):$(TAGS) .
7 | docker tag $(IMAGE):$(TAGS) $(IMAGE):latest
8 | @echo Successfully tagged $(IMAGE):$(TAGS) as latest
9 |
10 | .PHONY: push
11 | push: build
12 | docker push $(IMAGE):$(TAGS)
13 | docker push $(IMAGE):latest
14 | @echo Successfully pushed $(IMAGE):$(TAGS) as latest
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Quassel RESTSearch
2 |
3 | This is a websearch frontend for a quassel database.
4 |
5 | It offers both a simple HTTP API for search, and a normal website for the same purpose.
6 |
7 | Setting up search backends
8 | --------------------------
9 |
10 | #### pgsql-smart
11 |
12 | First, add a new column to the backlog table:
13 |
14 | ```sql
15 | ALTER TABLE backlog ADD COLUMN tsv tsvector;
16 | ```
17 |
18 | Second, add the two new indices:
19 |
20 | ```sql
21 | CREATE INDEX backlog_tsv_idx
22 | ON backlog
23 | USING gin(tsv);
24 | ```
25 |
26 | ```sql
27 | CREATE INDEX backlog_tsv_filtered_idx
28 | ON backlog
29 | USING gin(tsv)
30 | WHERE (type & 23559) > 0;
31 | ```
32 |
33 | Third, set up a trigger to populate the `tsv` column:
34 |
35 | ```sql
36 | CREATE TRIGGER tsvectorupdate
37 | BEFORE INSERT OR UPDATE
38 | ON backlog
39 | FOR EACH ROW
40 | EXECUTE PROCEDURE tsvector_update_trigger('tsv', 'pg_catalog.english', 'message');
41 | ```
42 |
43 | Fourth, populate the `tsv` column:
44 | ```sql
45 | UPDATE backlog
46 | SET messageid = messageid;
47 | ```
48 |
49 | Setting up the search
50 | ---------------------
51 |
52 | First, copy the file `qrs_config.default.php` to `qrs_config.php`.
53 |
54 | Then configure the database access, backend (currently only `pgsql-smart` is available), and the prefix of the path.
55 |
56 | Your qrs_config.php should look something like this
57 |
58 | ```injectablephp
59 | authenticate(
21 | $session->username ?: $_SERVER['PHP_AUTH_USER'] ?: '',
22 | $session->password ?: $_SERVER['PHP_AUTH_PW'] ?: ''
23 | )) {
24 | $session->destroy();
25 | $renderer->renderJsonError(false);
26 | } else {
27 | $renderer->renderJson($backend->context(
28 | param('anchor', 0),
29 | param('buffer', 0),
30 | param('before', 4),
31 | param('after', 4)
32 | ));
33 | }
34 |
--------------------------------------------------------------------------------
/api/search/index.php:
--------------------------------------------------------------------------------
1 | authenticate(
21 | $session->username ?: $_SERVER['PHP_AUTH_USER'] ?: '',
22 | $session->password ?: $_SERVER['PHP_AUTH_PW'] ?: ''
23 | )) {
24 | $session->destroy();
25 | $renderer->renderJsonError(false);
26 | } else {
27 | $renderer->renderJson($backend->find(
28 | param('query', ""),
29 | param('since'),
30 | param('before'),
31 | param('buffer'),
32 | param('network'),
33 | param('sender'),
34 | param('limit', 4)
35 | ));
36 | }
37 |
--------------------------------------------------------------------------------
/api/searchbuffer/index.php:
--------------------------------------------------------------------------------
1 | authenticate(
21 | $session->username ?: $_SERVER['PHP_AUTH_USER'] ?: '',
22 | $session->password ?: $_SERVER['PHP_AUTH_PW'] ?: ''
23 | )) {
24 | $session->destroy();
25 | $renderer->renderJsonError(false);
26 | } else {
27 | $renderer->renderJson($backend->findInBuffer(
28 | param('query', ""),
29 | param('since'),
30 | param('before'),
31 | param('sender'),
32 | param('buffer', 0),
33 | param('offset', 0),
34 | param('limit', 20)
35 | ));
36 | }
37 |
--------------------------------------------------------------------------------
/database/Config.php:
--------------------------------------------------------------------------------
1 | database_connector = $database_connector;
21 | $this->username = $username;
22 | $this->password = $password;
23 | $this->path_prefix = rtrim($path_prefix, "/");
24 | $this->backend = $backend;
25 | $this->database_options = $options;
26 | $this->enable_ranking = $enable_ranking;
27 | }
28 |
29 | public static function createFromGlobals()
30 | {
31 | $options = [];
32 | if (defined('qrs_db_option_tsqueryfunction') && (null !== qrs_db_option_tsqueryfunction)) {
33 | $options['tsqueryfunction'] = qrs_db_option_tsqueryfunction;
34 | }
35 |
36 | $options['timeout'] = (defined('qrs_db_option_timeout') && (null !== qrs_db_option_timeout)) ? qrs_db_option_timeout : 5000;
37 |
38 | if (defined('qrs_db_connector') && null !== qrs_db_connector)
39 | return new Config(qrs_path_prefix, qrs_db_connector, qrs_db_user, qrs_db_pass, qrs_backend, $options, qrs_enable_ranking);
40 | else
41 | return new Config(qrs_path_prefix, 'pgsql:host=' . qrs_db_host . ';port=' . qrs_db_port . ';dbname=' . qrs_db_name . '', qrs_db_user, qrs_db_pass, qrs_backend, $options, qrs_enable_ranking);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/database/Database.php:
--------------------------------------------------------------------------------
1 | backend = BackendFactory::create($type, new \PDO($database_connector, $username, $password), $options, $enable_ranking);
22 | $this->enable_ranking = $enable_ranking;
23 | }
24 |
25 | public static function createFromConfig(Config $config): Database
26 | {
27 | return Database::createFromOptions($config->database_connector, $config->username, $config->password, $config->backend, $config->database_options, $config->enable_ranking);
28 | }
29 |
30 | public static function createFromOptions(string $database_connector, string $username, string $password, string $type, array $options, bool $enable_ranking): Database
31 | {
32 | return new Database($database_connector, $username, $password, $type, $options, $enable_ranking);
33 | }
34 |
35 | public function authenticateFromHeader(string $header): bool
36 | {
37 | $parsedHeader = AuthHelper::parseAuthHeader($header);
38 | return $this->authenticate($parsedHeader['username'], $parsedHeader['password']);
39 | }
40 |
41 | public function authenticate(string $username, string $password): bool
42 | {
43 | if (!isset($username) || !isset($password)) {
44 | syslog(LOG_ERR, "Username or password not set");
45 | return false;
46 | }
47 |
48 | $stmt = $this->backend->findUser();
49 |
50 | $stmt->bindParam(":username", $username);
51 | $stmt->execute();
52 | $result = $stmt->fetch(\PDO::FETCH_ASSOC);
53 |
54 | if ($result === FALSE) {
55 | syslog(LOG_ERR, "Couldn’t find user " . $username);
56 | return false;
57 | }
58 |
59 | $user = new User($result);
60 |
61 | if (!AuthHelper::initialAuthenticateUser($password, $user->password, $user->hashversion)) {
62 | syslog(LOG_ERR, "Password does not match for user " . $username);
63 | return false;
64 | }
65 |
66 | $this->user = $user;
67 | return true;
68 | }
69 |
70 | private function apply_config(\PDOStatement $stmt)
71 | {
72 | $values = [
73 | ":config_normalization" => 4,
74 | ":weight_content" => 4,
75 | ":weight_type" => 5,
76 | ":weight_time" => 1,
77 | ];
78 | foreach ($this->backend->rankingParameters() as $parameter) {
79 | $stmt->bindValue($parameter, $values[$parameter], PDO::PARAM_INT);
80 | }
81 | }
82 |
83 | public function find(string $query, string $since = null, string $before = null, string $buffer = null, string $network = null, string $sender = null, int $limitPerBuffer = 4): array
84 | {
85 | $truncatedLimit = max(min($limitPerBuffer, 10), 0);
86 |
87 | $messages = $this->findInBufferMultiple($query, $since, $before, $buffer, $network, $sender, $truncatedLimit);
88 | $hasMore = $this->findInBufferMultipleCount($query, $since, $before, $buffer, $network, $sender, 0, $truncatedLimit);
89 |
90 | $buffermap = [];
91 |
92 | foreach ($messages as $message) {
93 | if (!array_key_exists($message['bufferid'], $buffermap)) {
94 | $buffermap[$message['bufferid']] = [
95 | "bufferid" => $message['bufferid'],
96 | "buffername" => $message['buffername'],
97 | "networkname" => $message['networkname'],
98 | "messages" => []
99 | ];
100 | }
101 |
102 | array_push($buffermap[$message['bufferid']]['messages'], $message);
103 | }
104 |
105 | foreach ($hasMore as $hasMoreResult) {
106 | if (array_key_exists($hasMoreResult['bufferid'], $buffermap))
107 | $buffermap[$hasMoreResult['bufferid']]['hasmore'] = $hasMoreResult['hasmore'];
108 | }
109 |
110 | return array_values($buffermap);
111 | }
112 |
113 | public function findInBufferMultiple(string $query, string $since = null, string $before = null, string $buffer = null, string $network = null, string $sender = null, int $limit = 4): array
114 | {
115 | $ignore_since = $since === null;
116 | $ignore_before = $before === null;
117 | $ignore_network = $network === null;
118 | $ignore_buffer = $buffer === null;
119 | $ignore_sender = $sender === null;
120 |
121 | $stmt = $this->backend->findInBuffers();
122 | $this->apply_config($stmt);
123 |
124 | $stmt->bindParam(':userid', $this->user->userid, PDO::PARAM_INT);
125 | $stmt->bindParam(':query', $query, PDO::PARAM_STR);
126 | $stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
127 |
128 | $stmt->bindValue(':since', !$ignore_since ? (string)$since : "1970-01-01", PDO::PARAM_STR);
129 | $stmt->bindValue(':before', !$ignore_before ? (string)$before : "1970-01-01", PDO::PARAM_STR);
130 | $stmt->bindValue(':buffer', !$ignore_buffer ? (string)$buffer : "", PDO::PARAM_STR);
131 | $stmt->bindValue(':network', !$ignore_network ? (string)$network : "", PDO::PARAM_STR);
132 | $stmt->bindValue(':sender', !$ignore_sender ? (string)$sender : "", PDO::PARAM_STR);
133 | $stmt->bindParam(':ignore_since', $ignore_since, PDO::PARAM_INT);
134 | $stmt->bindParam(':ignore_before', $ignore_before, PDO::PARAM_INT);
135 | $stmt->bindParam(':ignore_buffer', $ignore_buffer, PDO::PARAM_INT);
136 | $stmt->bindParam(':ignore_network', $ignore_network, PDO::PARAM_INT);
137 | $stmt->bindParam(':ignore_sender', $ignore_sender, PDO::PARAM_INT);
138 |
139 | $success = $stmt->execute();
140 | $result = $stmt->fetchAll(\PDO::FETCH_ASSOC);
141 | return $result;
142 | }
143 |
144 | public function findInBufferMultipleCount(string $query, string $since = null, string $before = null, string $buffer = null, string $network = null, string $sender = null, int $offset = 0, int $limit = 4): array
145 | {
146 | $truncatedLimit = max(min($limit, 50), 0);
147 | $ignore_since = $since === null;
148 | $ignore_before = $before === null;
149 | $ignore_network = $network === null;
150 | $ignore_buffer = $buffer === null;
151 | $ignore_sender = $sender === null;
152 |
153 | $stmt = $this->backend->findInBuffersCount();
154 |
155 | $stmt->bindParam(':userid', $this->user->userid, PDO::PARAM_INT);
156 | $stmt->bindParam(':query', $query, PDO::PARAM_STR);
157 |
158 | $stmt->bindValue(':since', !$ignore_since ? (string)$since : "1970-01-01", PDO::PARAM_STR);
159 | $stmt->bindValue(':before', !$ignore_before ? (string)$before : "1970-01-01", PDO::PARAM_STR);
160 | $stmt->bindValue(':buffer', !$ignore_buffer ? (string)$buffer : "", PDO::PARAM_STR);
161 | $stmt->bindValue(':network', !$ignore_network ? (string)$network : "", PDO::PARAM_STR);
162 | $stmt->bindValue(':sender', !$ignore_sender ? (string)$sender : "", PDO::PARAM_STR);
163 | $stmt->bindParam(':ignore_since', $ignore_since, PDO::PARAM_INT);
164 | $stmt->bindParam(':ignore_before', $ignore_before, PDO::PARAM_INT);
165 | $stmt->bindParam(':ignore_buffer', $ignore_buffer, PDO::PARAM_INT);
166 | $stmt->bindParam(':ignore_network', $ignore_network, PDO::PARAM_INT);
167 | $stmt->bindParam(':ignore_sender', $ignore_sender, PDO::PARAM_INT);
168 |
169 | $stmt->bindParam(':limit', $truncatedLimit, PDO::PARAM_INT);
170 | $stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
171 |
172 | $success = $stmt->execute();
173 | $result = $stmt->fetchAll(\PDO::FETCH_ASSOC);
174 | return $result;
175 | }
176 |
177 | public function findInBuffer(string $query, string $since = null, string $before = null, string $sender = null, int $bufferid, int $offset = 0, int $limit = 20): array
178 | {
179 | $truncatedLimit = max(min($limit, 50), 0);
180 | $ignore_since = $since === null;
181 | $ignore_before = $before === null;
182 | $ignore_sender = $sender === null;
183 |
184 | $stmt = $this->backend->findInBuffer();
185 | $this->apply_config($stmt);
186 |
187 | $stmt->bindParam(':userid', $this->user->userid, PDO::PARAM_INT);
188 | $stmt->bindParam(':bufferid', $bufferid, PDO::PARAM_INT);
189 | $stmt->bindParam(':query', $query, PDO::PARAM_STR);
190 |
191 | $stmt->bindValue(':since', !$ignore_since ? (string)$since : "1970-01-01", PDO::PARAM_STR);
192 | $stmt->bindValue(':before', !$ignore_before ? (string)$before : "1970-01-01", PDO::PARAM_STR);
193 | $stmt->bindValue(':sender', !$ignore_sender ? (string)$sender : "", PDO::PARAM_STR);
194 | $stmt->bindParam(':ignore_since', $ignore_since, PDO::PARAM_INT);
195 | $stmt->bindParam(':ignore_before', $ignore_before, PDO::PARAM_INT);
196 | $stmt->bindParam(':ignore_sender', $ignore_sender, PDO::PARAM_INT);
197 |
198 | $stmt->bindParam(':limit', $truncatedLimit, PDO::PARAM_INT);
199 | $stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
200 |
201 | $stmt->execute();
202 | return [
203 | 'results' => $stmt->fetchAll(\PDO::FETCH_ASSOC),
204 | 'hasmore' => $this->findInBufferCount($query, $since, $before, $sender, $bufferid, $offset, $limit)
205 | ];
206 | }
207 |
208 | public function findInBufferCount(string $query, string $since = null, string $before = null, string $sender = null, int $bufferid, int $offset = 0, int $limit = 4): bool
209 | {
210 | $truncatedLimit = max(min($limit, 50), 0);
211 | $ignore_since = $since === null;
212 | $ignore_before = $before === null;
213 | $ignore_sender = $sender === null;
214 |
215 | $stmt = $this->backend->findInBufferCount();
216 |
217 | $stmt->bindParam(':userid', $this->user->userid, PDO::PARAM_INT);
218 | $stmt->bindParam(':bufferid', $bufferid, PDO::PARAM_INT);
219 | $stmt->bindParam(':query', $query, PDO::PARAM_STR);
220 |
221 | $stmt->bindValue(':since', !$ignore_since ? (string)$since : "1970-01-01", PDO::PARAM_STR);
222 | $stmt->bindValue(':before', !$ignore_before ? (string)$before : "1970-01-01", PDO::PARAM_STR);
223 | $stmt->bindValue(':sender', !$ignore_sender ? (string)$sender : "", PDO::PARAM_STR);
224 | $stmt->bindParam(':ignore_since', $ignore_since, PDO::PARAM_INT);
225 | $stmt->bindParam(':ignore_before', $ignore_before, PDO::PARAM_INT);
226 | $stmt->bindParam(':ignore_sender', $ignore_sender, PDO::PARAM_INT);
227 |
228 | $stmt->bindParam(':limit', $truncatedLimit, PDO::PARAM_INT);
229 | $stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
230 |
231 | $stmt->execute();
232 | return $stmt->fetchColumn();
233 | }
234 |
235 | public function context(int $anchor, int $buffer, int $loadBefore, int $loadAfter): array
236 | {
237 | return array_merge(array_reverse($this->before($anchor, $buffer, $loadBefore)), $this->after($anchor, $buffer, $loadAfter));
238 | }
239 |
240 | public function before(int $anchor, int $buffer, int $limit): array
241 | {
242 | $truncatedLimit = max(min($limit, 50), 0);
243 |
244 | $stmt = $this->backend->loadBefore();
245 |
246 | $stmt->bindParam(":userid", $this->user->userid, PDO::PARAM_INT);
247 | $stmt->bindParam(":bufferid", $buffer, PDO::PARAM_INT);
248 | $stmt->bindParam(":anchor", $anchor, PDO::PARAM_INT);
249 |
250 | $stmt->bindParam(":limit", $truncatedLimit, PDO::PARAM_INT);
251 |
252 | $stmt->execute();
253 | return $stmt->fetchAll(\PDO::FETCH_ASSOC);
254 | }
255 |
256 | public function after(int $anchor, int $buffer, int $limit): array
257 | {
258 | $truncatedLimit = max(min($limit, 50), 0);
259 |
260 | $stmt = $this->backend->loadAfter();
261 |
262 | $stmt->bindParam(":userid", $this->user->userid, PDO::PARAM_INT);
263 | $stmt->bindParam(":bufferid", $buffer, PDO::PARAM_INT);
264 | $stmt->bindParam(":anchor", $anchor, PDO::PARAM_INT);
265 |
266 | $stmt->bindParam(":limit", $truncatedLimit, PDO::PARAM_INT);
267 |
268 | $stmt->execute();
269 | return $stmt->fetchAll(\PDO::FETCH_ASSOC);
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/database/User.php:
--------------------------------------------------------------------------------
1 | userid = $userArray['userid'];
16 | $this->username = $userArray['username'];
17 | $this->password = $userArray['password'];
18 | $this->hashversion = $userArray['hashversion'];
19 | }
20 | }
--------------------------------------------------------------------------------
/database/backends/Backend.php:
--------------------------------------------------------------------------------
1 | db = $db;
16 | $timeout = $options["timeout"];
17 | $this->db->exec("SET statement_timeout = $timeout;");
18 | $this->options = $options;
19 | $this->enable_ranking = $enable_ranking;
20 | }
21 |
22 | public function findUser(): \PDOStatement
23 | {
24 | return $this->db->prepare("
25 | SELECT *
26 | FROM quasseluser
27 | WHERE quasseluser.username = :username
28 | ");
29 | }
30 |
31 | private function tsQueryFunction(): string
32 | {
33 | return array_key_exists('tsqueryfunction', $this->options) ? $this->options['tsqueryfunction'] : "plainto_tsquery('english', :query)";
34 | }
35 |
36 | function rankingParameters(): array
37 | {
38 | if ($this->enable_ranking) {
39 | return [
40 | ":config_normalization",
41 | ":weight_content",
42 | ":weight_type",
43 | ":weight_time"
44 | ];
45 | } else {
46 | return [
47 | ":weight_type",
48 | ":weight_time"
49 | ];
50 | }
51 | }
52 |
53 | private function rankingFunction(): string
54 | {
55 | if ($this->enable_ranking) {
56 | return "(
57 | (ts_rank_cd(tsv, query, :config_normalization) ^ (2 ^ :weight_content)) *
58 | ((CASE
59 | WHEN TYPE IN (1, 4) THEN 1.0
60 | WHEN TYPE IN (2, 1024, 2048, 4096, 16384) THEN 0.8
61 | WHEN TYPE IN (32, 64, 128, 256, 512, 32768, 65536) THEN 0.6
62 | WHEN TYPE IN (8, 16, 8192, 131072) THEN 0.4
63 | ELSE 0.2 END) ^ (2 ^ :weight_type)) *
64 | ((1 / (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) - EXTRACT(EPOCH FROM time))) ^ (2 ^ :weight_time))
65 | )";
66 | } else {
67 | return "(
68 | ((CASE
69 | WHEN TYPE IN (1, 4) THEN 1.0
70 | WHEN TYPE IN (2, 1024, 2048, 4096, 16384) THEN 0.8
71 | WHEN TYPE IN (32, 64, 128, 256, 512, 32768, 65536) THEN 0.6
72 | WHEN TYPE IN (8, 16, 8192, 131072) THEN 0.4
73 | ELSE 0.2 END) ^ (2 ^ :weight_type)) *
74 | ((1 / (EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) - EXTRACT(EPOCH FROM time))) ^ (2 ^ :weight_time))
75 | )";
76 | }
77 | }
78 |
79 | public function findInBuffers(): \PDOStatement
80 | {
81 | $tsQueryFunction = $this->tsQueryFunction();
82 | $rankingFunction = $this->rankingFunction();
83 | return $this->db->prepare("
84 | WITH matching_backlog AS (
85 | SELECT * FROM backlog WHERE tsv @@ $tsQueryFunction
86 | )
87 | SELECT
88 | ranked_messages.bufferid,
89 | ranked_messages.buffername,
90 | ranked_messages.networkname,
91 | ranked_messages.messageid,
92 | ranked_messages.type,
93 | ranked_messages.time,
94 | ranked_messages.sender,
95 | ts_headline(replace(replace(ranked_messages.message, '<', '<'), '>', '>'), query, 'HighlightAll=TRUE') AS message
96 | FROM
97 | (SELECT
98 | matching_messages.*,
99 | network.networkname,
100 | sender.sender,
101 | rank()
102 | OVER (
103 | PARTITION BY matching_messages.bufferid
104 | ORDER BY matching_messages.rank_value DESC
105 | ) AS rank,
106 | first_value(rank_value)
107 | OVER (
108 | PARTITION BY matching_messages.bufferid
109 | ORDER BY matching_messages.rank_value DESC
110 | ) AS max_rank_value
111 | FROM
112 | (SELECT
113 | backlog.messageid,
114 | backlog.bufferid,
115 | buffer.buffername,
116 | buffer.networkid,
117 | backlog.senderid,
118 | backlog.type,
119 | backlog.time,
120 | backlog.message,
121 | query,
122 | $rankingFunction AS rank_value
123 | FROM
124 | matching_backlog AS backlog
125 | JOIN buffer ON backlog.bufferid = buffer.bufferid
126 | , $tsQueryFunction query
127 | WHERE buffer.userid = :userid
128 | AND (:ignore_since::BOOLEAN OR backlog.time > :since::TIMESTAMP)
129 | AND (:ignore_before::BOOLEAN OR backlog.time < :before::TIMESTAMP)
130 | AND (:ignore_buffer::BOOLEAN OR buffer.buffername ~* :buffer)
131 | AND backlog.type & 23559 > 0
132 | ) matching_messages
133 | JOIN sender ON matching_messages.senderid = sender.senderid
134 | JOIN network ON matching_messages.networkid = network.networkid
135 | WHERE (:ignore_network::BOOLEAN OR network.networkname ~* :network)
136 | AND (:ignore_sender::BOOLEAN OR sender.sender ~* :sender)
137 | ) ranked_messages
138 | WHERE ranked_messages.rank <= :limit
139 | ORDER BY ranked_messages.max_rank_value DESC, ranked_messages.rank_value DESC
140 | ");
141 | }
142 |
143 | public function findInBuffersCount(): \PDOStatement
144 | {
145 | $tsQueryFunction = $this->tsQueryFunction();
146 | return $this->db->prepare("
147 | SELECT
148 | backlog.bufferid,
149 | COUNT(*) > (:limit::INT + :offset::INT) AS hasmore
150 | FROM
151 | backlog
152 | JOIN buffer ON backlog.bufferid = buffer.bufferid
153 | JOIN sender ON backlog.senderid = sender.senderid
154 | JOIN network ON buffer.networkid = network.networkid
155 | , $tsQueryFunction query
156 | WHERE buffer.userid = :userid
157 | AND (:ignore_since::BOOLEAN OR backlog.time > :since::TIMESTAMP)
158 | AND (:ignore_before::BOOLEAN OR backlog.time < :before::TIMESTAMP)
159 | AND (:ignore_buffer::BOOLEAN OR buffer.buffername ~* :buffer)
160 | AND (:ignore_network::BOOLEAN OR network.networkname ~* :network)
161 | AND (:ignore_sender::BOOLEAN OR sender.sender ~* :sender)
162 | AND backlog.tsv @@ query AND backlog.type & 23559 > 0
163 | GROUP BY backlog.bufferid;
164 | ");
165 | }
166 |
167 | public function findInBuffer(): \PDOStatement
168 | {
169 | $tsQueryFunction = $this->tsQueryFunction();
170 | $rankingFunction = $this->rankingFunction();
171 | return $this->db->prepare("
172 | WITH matching_backlog AS (
173 | SELECT * FROM backlog WHERE tsv @@ $tsQueryFunction
174 | )
175 | SELECT
176 | matching_messages.messageid,
177 | matching_messages.time,
178 | sender.sender,
179 | ts_headline(replace(replace(matching_messages.message, '<', '<'), '>', '>'), query, 'HighlightAll=TRUE') AS message
180 | FROM
181 | (SELECT
182 | backlog.messageid,
183 | backlog.bufferid,
184 | buffer.buffername,
185 | buffer.networkid,
186 | backlog.senderid,
187 | backlog.type,
188 | backlog.time,
189 | backlog.message,
190 | query,
191 | $rankingFunction AS rank_value
192 | FROM
193 | matching_backlog AS backlog
194 | JOIN buffer ON backlog.bufferid = buffer.bufferid
195 | , $tsQueryFunction query
196 | WHERE buffer.userid = :userid
197 | AND buffer.bufferid = :bufferid
198 | AND (:ignore_since::BOOLEAN OR backlog.time > :since::TIMESTAMP)
199 | AND (:ignore_before::BOOLEAN OR backlog.time < :before::TIMESTAMP)
200 | AND backlog.type & 23559 > 0
201 | ) matching_messages
202 | JOIN sender ON matching_messages.senderid = sender.senderid
203 | JOIN network ON matching_messages.networkid = network.networkid
204 | WHERE (:ignore_sender::BOOLEAN OR sender.sender ~* :sender)
205 | ORDER BY matching_messages.rank_value DESC
206 | LIMIT :limit
207 | OFFSET :offset
208 | ");
209 | }
210 |
211 | public function findInBufferCount(): \PDOStatement
212 | {
213 | $tsQueryFunction = $this->tsQueryFunction();
214 | return $this->db->prepare("
215 | SELECT
216 | COUNT(*) > (:limit::INT + :offset::INT) AS hasmore
217 | FROM
218 | backlog
219 | JOIN buffer ON backlog.bufferid = buffer.bufferid
220 | JOIN sender ON backlog.senderid = sender.senderid
221 | , $tsQueryFunction query
222 | WHERE buffer.userid = :userid
223 | AND backlog.bufferid = :bufferid
224 | AND (:ignore_since::BOOLEAN OR backlog.time > :since::TIMESTAMP)
225 | AND (:ignore_before::BOOLEAN OR backlog.time < :before::TIMESTAMP)
226 | AND (:ignore_sender::BOOLEAN OR sender.sender ~* :sender)
227 | AND backlog.tsv @@ query AND backlog.type & 23559 > 0
228 | GROUP BY backlog.bufferid;
229 | ");
230 | }
231 |
232 | public function loadAfter(): \PDOStatement
233 | {
234 | return $this->db->prepare("
235 | SELECT backlog.messageid,
236 | backlog.bufferid,
237 | buffer.buffername,
238 | sender.sender,
239 | backlog.type,
240 | backlog.time,
241 | network.networkname,
242 | backlog.message
243 | FROM
244 | backlog
245 | JOIN sender ON backlog.senderid = sender.senderid
246 | JOIN buffer ON backlog.bufferid = buffer.bufferid
247 | JOIN network ON buffer.networkid = network.networkid
248 | WHERE buffer.userid = :userid
249 | AND backlog.bufferid = :bufferid
250 | AND backlog.messageid > :anchor
251 | ORDER BY backlog.messageid ASC
252 | LIMIT :limit;
253 | ");
254 | }
255 |
256 | public function loadBefore(): \PDOStatement
257 | {
258 | return $this->db->prepare("
259 | SELECT * FROM (SELECT backlog.messageid,
260 | backlog.bufferid,
261 | buffer.buffername,
262 | sender.sender,
263 | backlog.type,
264 | backlog.time,
265 | network.networkname,
266 | backlog.message
267 | FROM
268 | backlog
269 | JOIN sender ON backlog.senderid = sender.senderid
270 | JOIN buffer ON backlog.bufferid = buffer.bufferid
271 | JOIN network ON buffer.networkid = network.networkid
272 | WHERE buffer.userid = :userid
273 | AND backlog.bufferid = :bufferid
274 | AND backlog.messageid < :anchor
275 | ORDER BY backlog.messageid DESC
276 | LIMIT :limit) t
277 | ORDER BY messageid ASC;
278 | ");
279 | }
280 | }
281 |
--------------------------------------------------------------------------------
/database/backends/SQLiteSmartBackend.php:
--------------------------------------------------------------------------------
1 | ', '') FROM backlog_fts WHERE backlog_fts MATCH ('message: ' || 'jeanmuch*')
24 | */
25 |
26 | class SQLiteSmartBackend implements Backend
27 | {
28 | private $db;
29 | private $options;
30 | private $enable_ranking;
31 | private $fts5_enabled;
32 | private $opening_tag_marker;
33 | private $closing_tag_marker;
34 | private $opening_tag;
35 | private $closing_tag;
36 |
37 | function __construct(\PDO $db, array $options, bool $enable_ranking)
38 | {
39 | $this->db = $db;
40 | $timeout = $options["timeout"];
41 | $this->db->exec("PRAGMA busy_timeout = $timeout;");
42 | $this->options = $options;
43 | $this->enable_ranking = $enable_ranking;
44 | $this->fts5_enabled = SQLiteSmartBackend::ensureFullTextSearchableDatabase($db);
45 | $this->opening_tag_marker = chr(2);
46 | $this->closing_tag_marker = chr(3);
47 | $this->opening_tag = $this->opening_tag_marker . 'b' . $this->closing_tag_marker;
48 | $this->closing_tag = $this->opening_tag_marker . '/b' . $this->closing_tag_marker;
49 | }
50 |
51 | private static function ensureFullTextSearchableDatabase(\PDO $db): bool
52 | {
53 | // Check fts5 is supported
54 | $stmt = $db->prepare("SELECT sqlite_compileoption_used('ENABLE_FTS5');");
55 |
56 | if (!$stmt->execute()) {
57 | return false;
58 | }
59 |
60 | $record = $stmt->fetch(\PDO::FETCH_NUM);
61 |
62 | if (!$record || (int)$record[0] !== 1) {
63 | return false;
64 | }
65 |
66 | // Check if the fts5 table already exists
67 | $stmt = $db->prepare("SELECT rowid FROM sqlite_master WHERE type='table' and name = 'backlog_fts'");
68 |
69 | if (!$stmt->execute()) {
70 | return false;
71 | }
72 |
73 | $record = $stmt->fetch(\PDO::FETCH_NUM);
74 |
75 | if ($record && $record[0] > 0) {
76 | return true;
77 | }
78 |
79 | // Create the fts5 table, its update triggers and populate the initial indexes
80 | try {
81 | $oldMode = $db->getAttribute(\PDO::ATTR_ERRMODE);
82 |
83 | $db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
84 | $db->beginTransaction();
85 |
86 | $db->exec("CREATE VIRTUAL TABLE backlog_fts USING fts5(message, content='backlog', content_rowid='messageid');");
87 |
88 | $db->exec("
89 | CREATE TRIGGER tsvectorupdate_ai AFTER INSERT ON backlog BEGIN
90 | INSERT INTO backlog_fts(rowid, message) VALUES (new.messageid, new.message);
91 | END;
92 | ");
93 |
94 | $db->exec("
95 | CREATE TRIGGER tsvectorupdate_ad AFTER DELETE ON backlog BEGIN
96 | INSERT INTO backlog_fts(backlog_fts, rowid, message) VALUES('delete', old.messageid, old.message);
97 | END;
98 | ");
99 |
100 | $db->exec("
101 | CREATE TRIGGER tsvectorupdate_au AFTER UPDATE ON backlog BEGIN
102 | INSERT INTO backlog_fts(backlog_fts, rowid, message) VALUES('delete', old.messageid, old.message);
103 | INSERT INTO backlog_fts(rowid, message) VALUES (new.messageid, new.message);
104 | END;
105 | ");
106 |
107 | $db->exec("INSERT INTO backlog_fts(backlog_fts) VALUES('rebuild');");
108 |
109 | $db->setAttribute(\PDO::ATTR_ERRMODE, $oldMode);
110 | $db->commit();
111 | return true;
112 | } catch (\PDOException $e) {
113 | $db->rollBack();
114 | return false;
115 | }
116 | }
117 |
118 | public function findUser(): \PDOStatement
119 | {
120 | return $this->db->prepare("
121 | SELECT *
122 | FROM quasseluser
123 | WHERE quasseluser.username = :username
124 | ");
125 | }
126 |
127 | function rankingParameters(): array
128 | {
129 | return [
130 | ":weight_content",
131 | ":weight_type",
132 | ":weight_time"
133 | ];
134 | }
135 |
136 | private function rankingFunction(): string
137 | {
138 | // TODO: Properly port missing support of Pow in sqlite
139 |
140 | if ($this->enable_ranking) {
141 | return "(
142 | (matching_backlog.rank << :weight_content) *
143 | ((CASE
144 | WHEN type IN (1, 4) THEN 1.0
145 | WHEN type IN (2, 1024, 2048, 4096, 16384) THEN 0.8
146 | WHEN type IN (32, 64, 128, 256, 512, 32768, 65536) THEN 0.6
147 | WHEN type IN (8, 16, 8192, 131072) THEN 0.4
148 | ELSE 0.2 END) << :weight_type) *
149 | ((1 / (CAST(strftime('%s', 'now') AS INT) - (time / 1000))) << :weight_time)
150 | )";
151 | } else {
152 | return "(
153 | ((CASE
154 | WHEN type IN (1, 4) THEN 1.0
155 | WHEN type IN (2, 1024, 2048, 4096, 16384) THEN 0.8
156 | WHEN type IN (32, 64, 128, 256, 512, 32768, 65536) THEN 0.6
157 | WHEN type IN (8, 16, 8192, 131072) THEN 0.4
158 | ELSE 0.2 END) << :weight_type) *
159 | ((1 / (CAST(strftime('%s', 'now') AS INT) - (time / 1000))) << :weight_time)
160 | )";
161 | }
162 | }
163 |
164 | public function findInBuffers(): \PDOStatement
165 | {
166 | $rankingFunction = $this->rankingFunction();
167 | return $this->db->prepare("
168 | WITH matching_backlog AS (
169 | SELECT rowid, snippet(backlog_fts, 0, '$this->opening_tag', '$this->closing_tag', '...', 64) AS message, rank FROM backlog_fts WHERE backlog_fts MATCH ('message: ' || :query)
170 | )
171 | SELECT
172 | ranked_messages.bufferid,
173 | ranked_messages.buffername,
174 | ranked_messages.networkname,
175 | ranked_messages.messageid,
176 | ranked_messages.type,
177 | datetime(ranked_messages.time / 1000, 'unixepoch') AS time,
178 | ranked_messages.sender,
179 | replace(replace(replace(replace(ranked_messages.message, '<', '<'), '>', '>'), '$this->opening_tag_marker', '<'), '$this->closing_tag_marker', '>') AS message
180 | FROM
181 | (SELECT
182 | matching_messages.*,
183 | network.networkname,
184 | sender.sender,
185 | rank()
186 | OVER (
187 | PARTITION BY matching_messages.bufferid
188 | ORDER BY matching_messages.rank_value DESC
189 | ) AS rank,
190 | first_value(rank_value)
191 | OVER (
192 | PARTITION BY matching_messages.bufferid
193 | ORDER BY matching_messages.rank_value DESC
194 | ) AS max_rank_value
195 | FROM
196 | (SELECT
197 | backlog.messageid,
198 | backlog.bufferid,
199 | buffer.buffername,
200 | buffer.networkid,
201 | backlog.senderid,
202 | backlog.type,
203 | backlog.time,
204 | matching_backlog.message,
205 | $rankingFunction AS rank_value
206 | FROM
207 | backlog
208 | JOIN buffer ON backlog.bufferid = buffer.bufferid
209 | JOIN matching_backlog ON backlog.messageid = matching_backlog.rowid
210 | WHERE buffer.userid = :userid
211 | AND (:ignore_since OR backlog.time > CAST(strftime('%s', strftime('%Y-%m-%d', :since)) AS INT) * 1000)
212 | AND (:ignore_before OR backlog.time < CAST(strftime('%s', strftime('%Y-%m-%d', :before)) AS INT) * 1000)
213 | AND (:ignore_buffer OR buffer.buffername LIKE '%' || :buffer || '%')
214 | AND backlog.type & 23559 > 0
215 | ) matching_messages
216 | JOIN sender ON matching_messages.senderid = sender.senderid
217 | JOIN network ON matching_messages.networkid = network.networkid
218 | WHERE (:ignore_network OR network.networkname LIKE '%' || :network || '%')
219 | AND (:ignore_sender OR sender.sender LIKE '%' || :sender || '%')
220 | ) ranked_messages
221 | WHERE ranked_messages.rank <= :limit
222 | ORDER BY ranked_messages.max_rank_value DESC, ranked_messages.rank_value DESC;
223 | ");
224 | }
225 |
226 | public function findInBuffersCount(): \PDOStatement
227 | {
228 | return $this->db->prepare("
229 | WITH matching_backlog AS (
230 | SELECT rowid FROM backlog_fts WHERE backlog_fts MATCH ('message: ' || :query)
231 | )
232 | SELECT
233 | backlog.bufferid,
234 | COUNT(*) > (:limit + :offset) AS hasmore
235 | FROM
236 | backlog
237 | JOIN buffer ON backlog.bufferid = buffer.bufferid
238 | JOIN sender ON backlog.senderid = sender.senderid
239 | JOIN network ON buffer.networkid = network.networkid
240 | JOIN matching_backlog ON backlog.messageid = matching_backlog.rowid
241 | WHERE buffer.userid = :userid
242 | AND (:ignore_since OR backlog.time > CAST(strftime('%s', strftime('%Y-%m-%d', :since)) AS INT) * 1000)
243 | AND (:ignore_before OR backlog.time < CAST(strftime('%s', strftime('%Y-%m-%d', :before)) AS INT) * 1000)
244 | AND (:ignore_buffer OR buffer.buffername LIKE '%' || :buffer || '%')
245 | AND (:ignore_network OR network.networkname LIKE '%' || :network || '%')
246 | AND (:ignore_sender OR sender.sender LIKE '%' || :sender || '%')
247 | AND backlog.type & 23559 > 0
248 | GROUP BY backlog.bufferid;
249 | ");
250 | }
251 |
252 | public function findInBuffer(): \PDOStatement
253 | {
254 | $rankingFunction = $this->rankingFunction();
255 | return $this->db->prepare("
256 | WITH matching_backlog AS (
257 | SELECT rowid, snippet(backlog_fts, 0, '$this->opening_tag', '$this->closing_tag', '...', 64) AS message, rank FROM backlog_fts WHERE backlog_fts MATCH ('message: ' || :query)
258 | )
259 | SELECT
260 | matching_messages.messageid,
261 | matching_messages.time,
262 | datetime(matching_messages.time / 1000, 'unixepoch') AS time,
263 | sender.sender,
264 | replace(replace(replace(replace(matching_messages.message, '<', '<'), '>', '>'), '$this->opening_tag_marker', '<'), '$this->closing_tag_marker', '>') AS message
265 | FROM
266 | (SELECT
267 | backlog.messageid,
268 | backlog.bufferid,
269 | buffer.buffername,
270 | buffer.networkid,
271 | backlog.senderid,
272 | backlog.type,
273 | backlog.time,
274 | matching_backlog.message,
275 | $rankingFunction AS rank_value
276 | FROM
277 | backlog
278 | JOIN buffer ON backlog.bufferid = buffer.bufferid
279 | JOIN matching_backlog ON backlog.messageid = matching_backlog.rowid
280 | WHERE buffer.userid = :userid
281 | AND buffer.bufferid = :bufferid
282 | AND (:ignore_since OR backlog.time > CAST(strftime('%s', strftime('%Y-%m-%d', :since)) AS INT) * 1000)
283 | AND (:ignore_before OR backlog.time < CAST(strftime('%s', strftime('%Y-%m-%d', :before)) AS INT) * 1000)
284 | AND backlog.type & 23559 > 0
285 | ) matching_messages
286 | JOIN sender ON matching_messages.senderid = sender.senderid
287 | JOIN network ON matching_messages.networkid = network.networkid
288 | WHERE (:ignore_sender OR sender.sender LIKE '%' || :sender || '%')
289 | ORDER BY matching_messages.rank_value DESC
290 | LIMIT :limit
291 | OFFSET :offset
292 | ");
293 | }
294 |
295 | public function findInBufferCount(): \PDOStatement
296 | {
297 | return $this->db->prepare("
298 | WITH matching_backlog AS (
299 | SELECT rowid FROM backlog_fts WHERE backlog_fts MATCH ('message: ' || :query)
300 | )
301 | SELECT
302 | COUNT(*) > (:limit + :offset) AS hasmore
303 | FROM
304 | backlog
305 | JOIN buffer ON backlog.bufferid = buffer.bufferid
306 | JOIN sender ON backlog.senderid = sender.senderid
307 | JOIN matching_backlog ON backlog.messageid = matching_backlog.rowid
308 | WHERE buffer.userid = :userid
309 | AND backlog.bufferid = :bufferid
310 | AND (:ignore_since OR backlog.time > CAST(strftime('%s', strftime('%Y-%m-%d', :since)) AS INT) * 1000)
311 | AND (:ignore_before OR backlog.time < CAST(strftime('%s', strftime('%Y-%m-%d', :before)) AS INT) * 1000)
312 | AND (:ignore_sender OR sender.sender LIKE '%' || :sender || '%')
313 | AND backlog.type & 23559 > 0
314 | GROUP BY backlog.bufferid;
315 | ");
316 | }
317 |
318 | public function loadAfter(): \PDOStatement
319 | {
320 | return $this->db->prepare("
321 | SELECT backlog.messageid,
322 | backlog.bufferid,
323 | buffer.buffername,
324 | sender.sender,
325 | datetime(backlog.time / 1000, 'unixepoch') AS time,
326 | network.networkname,
327 | replace(replace(replace(backlog.message, '&', '&'), '<', '<'), '>', '>') AS message
328 | FROM
329 | backlog
330 | JOIN sender ON backlog.senderid = sender.senderid
331 | JOIN buffer ON backlog.bufferid = buffer.bufferid
332 | JOIN network ON buffer.networkid = network.networkid
333 | WHERE buffer.userid = :userid
334 | AND backlog.bufferid = :bufferid
335 | AND backlog.messageid > :anchor
336 | ORDER BY backlog.messageid ASC
337 | LIMIT :limit;
338 | ");
339 | }
340 |
341 | public function loadBefore(): \PDOStatement
342 | {
343 | return $this->db->prepare("
344 | SELECT * FROM (SELECT backlog.messageid,
345 | backlog.bufferid,
346 | buffer.buffername,
347 | sender.sender,
348 | datetime(backlog.time / 1000, 'unixepoch') AS time,
349 | network.networkname,
350 | replace(replace(replace(backlog.message, '&', '&'), '<', '<'), '>', '>') AS message
351 | FROM
352 | backlog
353 | JOIN sender ON backlog.senderid = sender.senderid
354 | JOIN buffer ON backlog.bufferid = buffer.bufferid
355 | JOIN network ON buffer.networkid = network.networkid
356 | WHERE buffer.userid = :userid
357 | AND backlog.bufferid = :bufferid
358 | AND backlog.messageid < :anchor
359 | ORDER BY backlog.messageid DESC
360 | LIMIT :limit) t
361 | ORDER BY messageid ASC;
362 | ");
363 | }
364 | }
365 |
--------------------------------------------------------------------------------
/database/helper/AuthHelper.php:
--------------------------------------------------------------------------------
1 | base64_decode($arr[0]),
57 | 'password' => base64_decode($arr[1])
58 | ];
59 | }
60 | }
61 |
62 | public static function generateAuthHeader(string $password, string $username): string
63 | {
64 | return base64_encode(base64_encode($username) . ":" . base64_encode($password));
65 | }
66 | }
--------------------------------------------------------------------------------
/database/helper/RendererHelper.php:
--------------------------------------------------------------------------------
1 | config = $config;
17 | $this->translator = new TranslationHelper($config);
18 | $this->sessionHelper = $sessionHelper;
19 | }
20 |
21 | public function renderError($e)
22 | {
23 | header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
24 | header('Status: 403 Forbidden');
25 | echo 'Error 403: Forbidden' . "\n";
26 | echo $e . "\n";
27 | }
28 |
29 | public function renderJsonError($json)
30 | {
31 | header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden');
32 | header('Status: 403 Forbidden');
33 | header('Content-Type: application/json');
34 | echo json_encode($json) . "\n";
35 | }
36 |
37 | public function renderJson($json)
38 | {
39 | header('Content-Type: application/json');
40 | echo json_encode($json) . "\n";
41 | }
42 |
43 | public function renderPage(string $template, array $vars = [])
44 | {
45 | $translation = $this->translator->loadTranslation($this->translator->findMatchingLanguage($_SERVER['HTTP_ACCEPT_LANGUAGE']));
46 | $viewHelper = new ViewHelper($translation, array_merge($this->sessionHelper->vars, $vars));
47 | $viewHelper->render($template);
48 | $this->sessionHelper->vars = [];
49 | }
50 |
51 | public function redirect(string $page, array $vars = [])
52 | {
53 | header('Location: ' . $this->config->path_prefix . $page);
54 | $this->sessionHelper->startSession();
55 | $this->sessionHelper->vars = $vars;
56 | }
57 | }
--------------------------------------------------------------------------------
/database/helper/SessionHelper.php:
--------------------------------------------------------------------------------
1 | startSession();
26 |
27 | return self::$instance;
28 | }
29 |
30 |
31 | public function startSession(): bool
32 | {
33 | if ($this->sessionState == self::SESSION_NOT_STARTED) {
34 | $this->sessionState = session_start();
35 | }
36 |
37 | return $this->sessionState;
38 | }
39 |
40 | public function __get(string $name)
41 | {
42 | if (isset($_SESSION[$name])) {
43 | return $_SESSION[$name];
44 | } else {
45 | return null;
46 | }
47 | }
48 |
49 | public function __set(string $name, $value)
50 | {
51 | $_SESSION[$name] = $value;
52 | }
53 |
54 | public function __isset(string $name): bool
55 | {
56 | return isset($_SESSION[$name]);
57 | }
58 |
59 |
60 | public function __unset(string $name)
61 | {
62 | unset($_SESSION[$name]);
63 | }
64 |
65 |
66 | public function destroy(): bool
67 | {
68 | if ($this->sessionState == self::SESSION_STARTED) {
69 | $this->sessionState = !session_destroy();
70 | unset($_SESSION);
71 |
72 | return !$this->sessionState;
73 | }
74 |
75 | return !$this->sessionState;
76 | }
77 | }
--------------------------------------------------------------------------------
/database/helper/TranslationHelper.php:
--------------------------------------------------------------------------------
1 | setPath('../../translations/');
12 | }
13 |
14 | public function setPath(string $path)
15 | {
16 | $this->template_dir = realpath(dirname(__FILE__) . '/' . $path);
17 | }
18 |
19 | public function findMatchingLanguage(string $language_str): string
20 | {
21 | $languages = explode(",", $language_str);
22 | foreach ($languages as $language) {
23 | $language = explode(";", $language)[0];
24 | if ($this->exists($language)) {
25 | return $language;
26 | }
27 | }
28 | return "en";
29 | }
30 |
31 | public function exists($language): bool
32 | {
33 | return file_exists($this->path($language));
34 | }
35 |
36 | private function path($language): string
37 | {
38 | return $this->template_dir . '/' . $language . '.json';
39 | }
40 |
41 | public function loadTranslation($language): array
42 | {
43 | return json_decode(file_get_contents($this->path($language)), true);
44 | }
45 | }
--------------------------------------------------------------------------------
/database/helper/ViewHelper.php:
--------------------------------------------------------------------------------
1 | translation = $translation;
14 | $this->setPath('../../templates/');
15 | if ($vars !== null) {
16 | $this->vars = $vars;
17 | }
18 | }
19 |
20 | public function setPath(string $path)
21 | {
22 | $this->template_dir = realpath(dirname(__FILE__) . '/' . $path);
23 | }
24 |
25 | public function render($template_file)
26 | {
27 | $translation = $this->translation;
28 | $t = function ($path) use ($translation) {
29 | $arr = explode(".", $path);
30 | $var = $translation;
31 | foreach ($arr as $key)
32 | $var = $var[$key];
33 | echo $var;
34 | };
35 | $vars = $this->vars;
36 |
37 | $path = $this->template_dir . '/' . $template_file . '.phtml';
38 | if (file_exists($path)) {
39 | include $path;
40 | } else {
41 | throw new \Exception('Template ' . $path . ' not found ');
42 | }
43 | }
44 |
45 | public function __get($name)
46 | {
47 | return $this->vars[$name];
48 | }
49 |
50 | public function __set($name, $value)
51 | {
52 | $this->vars[$name] = $value;
53 | }
54 | }
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justjanne/quassel-rest-search/014d624e17bdf06fa1711db3238819fe095a77f8/favicon.ico
--------------------------------------------------------------------------------
/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justjanne/quassel-rest-search/014d624e17bdf06fa1711db3238819fe095a77f8/favicon.png
--------------------------------------------------------------------------------
/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
22 |
--------------------------------------------------------------------------------
/index.php:
--------------------------------------------------------------------------------
1 | authenticate($session->username ?: '', $session->password ?: '')) {
16 | $session->destroy();
17 | $renderer->redirect('/login.php', ['message' => 'login.message.error_unauthed', 'type' => 'error']);
18 | } else {
19 | $renderer->renderPage('search', ['username' => $session->username]);
20 | }
--------------------------------------------------------------------------------
/login.php:
--------------------------------------------------------------------------------
1 | authenticate($username, $password)) {
19 | $session->username = $username;
20 | $session->password = $password;
21 | $renderer->redirect('/');
22 | } else {
23 | syslog(LOG_ERR, "Could not authenticate user " . $username);
24 | $renderer->redirect('/login.php', ['message' => 'login.message.error_invalid', 'type' => 'error']);
25 | }
26 | } elseif (isset($_GET['action']) && $_GET['action'] === 'logout') {
27 | $session->destroy();
28 | $renderer->redirect('/login.php', ['message' => 'login.message.success_logout', 'type' => 'info']);
29 | } else if ($backend->authenticate($session->username ?: '', $session->password ?: '')) {
30 | $renderer->redirect('/');
31 | } else {
32 | $renderer->renderPage('login');
33 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "quassel-rest-search",
3 | "version": "3.0.0",
4 | "description": "This is a websearch frontend for a quassel database.",
5 | "scripts": {
6 | "jsx": "node_modules/nativejsx/bin/nativejsx res/js/**/*.jsx",
7 | "dep": "cp node_modules/nativejsx/dist/nativejsx-prototypes.js res/js/util/",
8 | "sass": "node_modules/node-sass/bin/node-sass --style compressed res/css/ -o res/css/"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/justjanne/quassel-rest-search.git"
13 | },
14 | "author": "Janne Koschinski",
15 | "license": "GPL",
16 | "bugs": {
17 | "url": "https://github.com/justjanne/quassel-rest-search/issues"
18 | },
19 | "homepage": "https://github.com/justjanne/quassel-rest-search#readme",
20 | "devDependencies": {
21 | "acorn": ">=5.7.4",
22 | "nativejsx": "https://github.com/j3l11234/nativejsx/archive/4.2.0.2.tar.gz",
23 | "node-sass": "^7.0.3"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/qrs_config.default.php:
--------------------------------------------------------------------------------
1 | .container
42 | > .inline-button
43 | display: none
44 |
45 | .title
46 | border-bottom: 1px solid transparent
47 | border-top: 1px solid transparent
48 | color: #616161
49 | margin: 0
50 | padding: 20px 6px 10px 22px
51 | position: sticky
52 | top: 55px
53 | z-index: 1
54 | transition: all 400ms
55 | display: flex
56 |
57 | &:before
58 | content: ""
59 | display: block
60 | left: -16px
61 | right: -16px
62 | top: 0
63 | bottom: 0
64 | background-color: #f2f2f2
65 | position: absolute
66 | z-index: -2
67 |
68 | @media(max-width: 800px)
69 | left: 0
70 | right: 0
71 |
72 | &:after
73 | content: ""
74 | display: block
75 | left: 0
76 | right: 0
77 | top: 0
78 | bottom: 0
79 | background-color: transparent
80 | transition: margin-left 400ms, margin-right 400ms, background-color 400ms
81 | will-change: margin-left, margin-right, background-color
82 | position: absolute
83 | z-index: -1
84 |
85 | h2
86 | flex: 1
87 | line-height: 36px
88 |
89 | button
90 | border-radius: 3px
91 | box-shadow: none
92 | color: #444444
93 | text-transform: uppercase
94 | border: none
95 | position: relative
96 | font-weight: normal
97 | margin-bottom: 0
98 | text-align: center
99 | vertical-align: middle
100 | touch-action: manipulation
101 | background: transparent none
102 | white-space: nowrap
103 | padding: 6px 16px
104 | font-size: 13px
105 | line-height: 1.846
106 | @include vendor-prefix('user-select', 'none')
107 | text-decoration: none
108 | box-sizing: border-box
109 | font-family: "Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif
110 | will-change: background
111 | opacity: 0
112 | pointer-events: none
113 | cursor: none
114 | transition: opacity 0.4s 0.2s, background 0.4s
115 |
116 | &:hover
117 | background-color: rgba(0, 0, 0, 0.06)
118 |
119 | &:after
120 | content: ""
121 | display: block
122 | position: absolute
123 | width: 100%
124 | height: 100%
125 | top: 0
126 | left: 0
127 | background: radial-gradient(circle, #444444 10%, transparent 10.01%) no-repeat 50%
128 | background-size: 1000% 1000%
129 | opacity: 0
130 | pointer-events: none
131 | transition: background .5s, opacity 1s
132 |
133 | &:active
134 | &:after
135 | background-size: 0 0
136 | opacity: .2
137 | transition: 0s
138 |
139 | &:not(.focus) .secondary
140 | display: none
141 |
142 | &:before
143 | content: ""
144 | display: block
145 | left: 0
146 | right: 0
147 | top: 0
148 | bottom: 0
149 | background-color: transparent
150 | transition: margin-left 400ms, margin-right 400ms, margin-bottom 400ms, background-color 400ms
151 | will-change: margin-left, margin-right, margin-bottom, background-color
152 | position: absolute
153 | z-index: -1
154 |
155 | &.focus
156 | &:before
157 | margin-bottom: -16px
158 | background-color: #ddd
159 |
160 | @media(min-width: 800px)
161 | margin-left: -16px
162 | margin-right: -16px
163 |
164 | .title
165 | &:after
166 | background-color: #ddd
167 |
168 | @media(min-width: 800px)
169 | margin-left: -16px
170 | margin-right: -16px
171 |
172 | button
173 | opacity: 1
174 | display: inline-block
175 | cursor: pointer
176 | pointer-events: initial
177 |
178 | .container
179 | font-size: 13px
180 |
181 | > .inline-button
182 | overflow: hidden
183 | font-size: 14px
184 | line-height: 24px
185 | padding: 7px 24px
186 | border-bottom: 1px solid #e5e5e5
187 | color: #555
188 | background: #f0f0f0
189 | cursor: pointer
190 | @include vendor-prefix('user-select', 'none')
191 | transition: height 400ms
192 | position: relative
193 | box-shadow: 0 -1px 0 #e5e5e5, 0 0 2px rgba(0, 0, 0, .12), 0 2px 4px rgba(0, 0, 0, .24)
194 |
195 | &:before
196 | content: ""
197 | display: inline-block
198 | height: 24px
199 | width: 24px
200 | background-image: url(../icons/dots-horizontal.svg)
201 | background-size: cover
202 | vertical-align: top
203 | margin-right: 16px
204 |
205 | &.before> .inline-button:before
206 | background-image: url(../icons/chevron-up.svg)
207 |
208 | &.after > .inline-button:before
209 | background-image: url(../icons/chevron-down.svg)
210 |
211 | .context
212 | display: block
213 | position: relative
214 | animation-duration: 1s
215 | animation-name: slidein
216 |
217 | .container .inline-button:first-child:last-child
218 | display: none
219 |
220 | &.focus
221 | padding: 0
222 | margin: 0.5rem -0.5rem
223 |
224 | @media(max-width: 800px)
225 | margin: 1rem 0
226 |
227 | .container.before
228 | .message, .inline-button
229 | animation-name: slidein_msg_before
230 | animation-duration: 400ms
231 |
232 | .container.after
233 | .message, .inline-button
234 | animation-name: slidein_msg_after
235 | animation-duration: 400ms
236 |
237 | &:not(.focus)
238 | > .before, > .after
239 | display: none
240 |
241 | > .inline-button
242 | display: none
243 |
244 | .message
245 | display: flex
246 | line-height: 20px
247 | padding: 12px 24px
248 | border-bottom: 1px solid #e5e5e5
249 | color: #212121
250 | background: #fff
251 | position: relative
252 |
253 | .invisible
254 | font-size: 0
255 | opacity: 0
256 |
257 | &:before
258 | bottom: 0
259 | box-shadow: 0 -1px 0 #e5e5e5, 0 0 2px rgba(0, 0, 0, .12), 0 2px 4px rgba(0, 0, 0, .24)
260 | content: ''
261 | display: block
262 | left: 0
263 | pointer-events: none
264 | position: absolute
265 | right: 0
266 | top: 0
267 |
268 | > .more
269 | height: 56px
270 | width: 56px
271 | position: relative
272 | padding: 16px 13px
273 | cursor: pointer
274 | color: #555
275 | transition: all .15s
276 | text-decoration: none
277 | text-align: center
278 | margin-right: -24px
279 | margin-top: -12px
280 | @include vendor-prefix('user-select', 'none')
281 |
282 | &:before
283 | background-color: rgba(255, 255, 255, .12)
284 | bottom: 9px
285 | content: ' '
286 | left: 9px
287 | margin: auto
288 | padding: 4px
289 | position: absolute
290 | right: 9px
291 | top: 9px
292 | z-index: -1
293 | border-radius: 50%
294 | opacity: 0
295 | transition: opacity 100ms
296 |
297 | &:hover:before
298 | opacity: 1
299 |
300 | time
301 | width: 136px
302 | display: inline-block
303 | text-align: right
304 | flex-shrink: 0
305 | font-family: monospace
306 | font-size: 85%
307 | color: #444
308 |
309 | @media(max-width: 800px)
310 | position: absolute
311 | right: 8px
312 | bottom: 8px
313 | width: initial
314 | font-style: italic
315 | color: #777
316 |
317 | .container
318 | display: flex
319 | flex-grow: 1
320 | flex-shrink: 1
321 | overflow: hidden
322 |
323 | @media(max-width: 800px)
324 | display: block
325 | padding-bottom: 16px
326 |
327 | .sender
328 | width: 148px
329 | display: inline-block
330 | padding: 0 24px
331 | font-weight: bold
332 | flex-shrink: 0
333 | overflow: hidden
334 | text-overflow: ellipsis
335 |
336 | @media(max-width: 800px)
337 | width: initial
338 | padding: 0
339 | display: inline
340 | line-height: 1
341 |
342 | &:after
343 | @media(max-width: 800px)
344 | content: ': '
345 |
346 | .content
347 | flex-grow: 1
348 | overflow: hidden
349 |
350 | .irc_highlight
351 | background-color: rgba(251, 246, 167, 0.5)
352 | color: #212121 !important
353 |
354 | @media(max-width: 800px)
355 | vertical-align: top
356 | display: inline
357 |
358 | span
359 | vertical-align: top
360 | &.action
361 | .sender, .content
362 | font-style: italic
363 |
364 | .sender
365 | &:before
366 | content: ' -*- '
367 | color: #1a237e
368 |
369 | .content
370 | color: #1a237e
371 |
372 | &.notice
373 | .content, .sender
374 | color: #916409
375 |
376 | .sender
377 | &:before
378 | content: '['
379 | &:after
380 | content: ']'
--------------------------------------------------------------------------------
/res/css/_font.sass:
--------------------------------------------------------------------------------
1 | @font-face
2 | font-family: 'Roboto'
3 | font-style: normal
4 | font-weight: 400
5 | src: local('Roboto'), local('Roboto-Regular'), url('../fonts/roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-regular.woff2') format('woff2'), url('../fonts/roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-regular.woff') format('woff')
6 |
7 | @font-face
8 | font-family: 'Roboto'
9 | font-style: normal
10 | font-weight: 700
11 | src: local('Roboto Bold'), local('Roboto-Bold'), url('../fonts/roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-700.woff2') format('woff2'), url('../fonts/roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-700.woff') format('woff')
--------------------------------------------------------------------------------
/res/css/_icons.sass:
--------------------------------------------------------------------------------
1 | @font-face
2 | font-family: 'Material Icons'
3 | font-style: normal
4 | font-weight: 400
5 | src: local('Material Icons'), local('MaterialIcons-Regular'), url(../icons/MaterialIcons-Regular.woff2) format('woff2'), url(../icons/MaterialIcons-Regular.woff) format('woff')
6 |
7 | .icon
8 | font-family: 'Material Icons', sans-serif
9 | font-weight: normal
10 | font-style: normal
11 | font-size: 24px
12 | display: inline-block
13 | width: 1em
14 | height: 1em
15 | line-height: 1
16 | text-transform: none
17 | letter-spacing: normal
18 | word-wrap: normal
19 | white-space: nowrap
20 | direction: ltr
21 |
22 | /* Support for all WebKit browsers. */
23 | -webkit-font-smoothing: antialiased
24 | /* Support for Safari and Chrome. */
25 | text-rendering: optimizeLegibility
26 |
27 | /* Support for Firefox. */
28 | -moz-osx-font-smoothing: grayscale
29 |
30 | /* Support for IE. */
31 | font-feature-settings: 'liga'
--------------------------------------------------------------------------------
/res/css/_loading.sass:
--------------------------------------------------------------------------------
1 | .progress
2 | position: absolute
3 | height: 4px
4 | display: block
5 | background-color: rgba(255,255,255,0.2)
6 | overflow: hidden
7 | transition: opacity 400ms
8 | bottom: 0
9 | left: 0
10 | right: 0
11 |
12 | &:not(.visible)
13 | opacity: 0
14 |
15 | .indeterminate
16 | background-color: rgba(255,255,255,0.8)
17 |
18 | &:before
19 | content: ''
20 | position: absolute
21 | background-color: inherit
22 | top: 0
23 | left: 0
24 | bottom: 0
25 | will-change: left, right
26 | animation: indeterminate 4.2s cubic-bezier(0.65, 0.815, 0.735, 0.395) infinite
27 |
28 | &:after
29 | content: ''
30 | position: absolute
31 | background-color: inherit
32 | top: 0
33 | left: 0
34 | bottom: 0
35 | will-change: left, right
36 | animation: indeterminate-short 4.2s cubic-bezier(0.165, 0.84, 0.44, 1) infinite
37 | animation-delay: 2.25s
38 |
39 |
--------------------------------------------------------------------------------
/res/css/_mirccolor.sass:
--------------------------------------------------------------------------------
1 | .irc_bold
2 | font-weight: bold
3 |
4 | .irc_italic
5 | font-style: italic
6 |
7 | .irc_underline
8 | text-decoration: underline
9 |
10 | .irc_strikethrough
11 | text-decoration: line-through
12 |
13 | .irc_strikethrough.irc_underline
14 | text-decoration: line-through underline
15 |
16 | .irc_monospace
17 | font-family: 'Roboto Mono', 'Source Code Pro', monospace
18 |
19 | $mircColors: ("0": #ffffff, "1": #000000, "2": #000080, "3": #008000, "4": #ff0000, "5": #800000, "6": #800080, "7": #ffa500, "8": #ffff00, "9": #00ff00, "10": #008080, "11": #00ffff, "12": #4169e1, "13": #ff00ff, "14": #808080, "15": #c0c0c0, "16": #470000, "17": #472100, "18": #474700, "19": #324700, "20": #004700, "21": #00472c, "22": #004747, "23": #002747, "24": #000047, "25": #2e0047, "26": #470047, "27": #47002a, "28": #740000, "29": #743a00, "30": #747400, "31": #517400, "32": #007400, "33": #007449, "34": #007474, "35": #004074, "36": #000074, "37": #4b0074, "38": #740074, "39": #740045, "40": #b50000, "41": #b56300, "42": #b5b500, "43": #7db500, "44": #00b500, "45": #00b571, "46": #00b5b5, "47": #0063b5, "48": #0000b5, "49": #7500b5, "50": #b500b5, "51": #b5006b, "52": #ff0000, "53": #ff8c00, "54": #ffff00, "55": #b2ff00, "56": #00ff00, "57": #00ffa0, "58": #00ffff, "59": #008cff, "60": #0000ff, "61": #a500ff, "62": #ff00ff, "63": #ff0098, "64": #ff5959, "65": #ffb459, "66": #ffff71, "67": #cfff60, "68": #6fff6f, "69": #65ffc9, "70": #6dffff, "71": #59b4ff, "72": #5959ff, "73": #c459ff, "74": #ff66ff, "75": #ff59bc, "76": #ff9c9c, "77": #ffd39c, "78": #ffff9c, "79": #e2ff9c, "80": #9cff9c, "81": #9cffdb, "82": #9cffff, "83": #9cd3ff, "84": #9c9cff, "85": #dc9cff, "86": #ff9cff, "87": #ff94d3, "88": #000000, "89": #131313, "90": #282828, "91": #363636, "92": #4d4d4d, "93": #656565, "94": #818181, "95": #9f9f9f, "96": #bcbcbc, "97": #e2e2e2, "98": #ffffff)
20 |
21 | @each $i, $color in $mircColors
22 | [data-irc_foreground="#{$i}"]
23 | color: $color
24 |
25 | [data-irc_background="#{$i}"]
26 | background-color: $color
--------------------------------------------------------------------------------
/res/css/_nav.sass:
--------------------------------------------------------------------------------
1 | @import "util"
2 |
3 | .nav
4 | position: fixed
5 | left: 0
6 | right: 0
7 | top: 0
8 | height: 56px
9 | z-index: 2
10 |
11 | .bar
12 | background-color: #0a70c0
13 | z-index: 1
14 | position: absolute
15 | top: 0
16 | left: 0
17 | right: 0
18 | box-shadow: 0 -1px 0 #e0e0e0, 0 0 2px rgba(0, 0, 0, .12), 0 2px 4px rgba(0, 0, 0, .24)
19 | transition: background-color .15s
20 | will-change: background-color
21 |
22 | .container
23 | max-width: 1136px
24 | margin: 0 auto
25 | padding: 0 2rem
26 | display: flex
27 |
28 | @media(max-width: 800px)
29 | padding: 0 0 0 8px
30 |
31 | .searchBar
32 | margin: 8px auto
33 | height: 40px
34 | display: flex
35 | position: relative
36 | border: none
37 | background: #348bcf
38 | border-radius: 2px
39 | padding: 0 0 0 72px
40 | color: #ffffff
41 | flex-direction: row
42 | flex: 1
43 | box-shadow: 0 1px 1px rgba(0, 0, 0, .06)
44 | transition: background-color .15s
45 |
46 | &:hover
47 | background-color: rgba(255, 255, 255, .3)
48 |
49 | .icon
50 | display: inline-block
51 | width: 72px
52 | height: 40px
53 | position: absolute
54 | text-align: center
55 | line-height: 40px
56 | left: 0
57 | top: 0
58 | cursor: pointer
59 |
60 | .search
61 | display: inline-block
62 | flex-grow: 1
63 | flex-shrink: 1
64 | background: none
65 | border: none
66 | line-height: 100%
67 | color: #ffffff
68 | font-size: 1rem
69 | margin-right: 8px
70 | -webkit-appearance: textfield
71 | -webkit-box-sizing: content-box
72 |
73 | &::-moz-placeholder
74 | color: #ffffff
75 | opacity: 1
76 | -moz-osx-font-smoothing: grayscale
77 |
78 | &::-webkit-input-placeholder
79 | color: #ffffff
80 | opacity: 1
81 | -webkit-font-smoothing: antialiased
82 |
83 | &::-webkit-search-decoration, &::-webkit-search-cancel-button
84 | display: none
85 |
86 | .actions
87 | display: flex
88 | align-items: center
89 | justify-content: flex-end
90 |
91 | a
92 | height: 56px
93 | width: 56px
94 | position: relative
95 | padding: 16px 13px
96 | cursor: pointer
97 | color: #fff
98 | transition: all .15s
99 | text-decoration: none
100 | text-align: center
101 | @include vendor-prefix('user-select', 'none')
102 |
103 | &:before
104 | background-color: rgba(255, 255, 255, .12)
105 | bottom: 9px
106 | content: ' '
107 | left: 9px
108 | margin: auto
109 | padding: 4px
110 | position: absolute
111 | right: 9px
112 | top: 9px
113 | z-index: -1
114 | border-radius: 50%
115 | opacity: 0
116 | transition: opacity 100ms
117 |
118 | &:hover:before
119 | opacity: 1
120 |
121 | .history
122 | top: 100%
123 | max-width: 1136px
124 | margin: auto
125 | padding: 0 2rem
126 | transform: translateY(-200%)
127 | transition: transform 400ms
128 | position: relative
129 |
130 | @media(max-width: 800px)
131 | padding: 0
132 |
133 | ul
134 | list-style-type: none
135 | margin: 0
136 | padding: 6px 0
137 | background: #fff
138 | box-shadow: 0 -1px 0 #e0e0e0, 0 0 2px rgba(0, 0, 0, .12), 0 2px 4px rgba(0, 0, 0, .24)
139 |
140 | li
141 | cursor: pointer
142 | line-height: 36px
143 | overflow: hidden
144 | padding: 0 24px
145 | text-overflow: ellipsis
146 | white-space: nowrap
147 |
148 | &:hover, &:focus, &.focus
149 | background: rgba(0, 0, 0, 0.03)
150 |
151 | .icon
152 | border-radius: 12px
153 | height: 24px
154 | margin-right: 24px
155 | vertical-align: middle
156 | width: 24px
157 | display: inline-block
158 | background-size: cover
159 | opacity: 0.6
160 |
161 | p
162 | cursor: default
163 | line-height: 36px
164 | overflow: hidden
165 | padding: 0 24px
166 | text-overflow: ellipsis
167 | white-space: nowrap
168 | font-style: italic
169 | color: #646464
170 |
171 | &.focus
172 | .bar
173 | background: #f2f2f2
174 |
175 | .container
176 |
177 | .searchBar
178 | background: #ffffff
179 |
180 | .search
181 | color: #333333
182 |
183 | &::-moz-placeholder
184 | color: #757575
185 |
186 | &::-webkit-input-placeholder
187 | color: #757575
188 |
189 | .icon
190 | color: #757575
191 |
192 | .progress
193 | background-color: rgba(117, 117, 117, 0.2)
194 |
195 | .indeterminate
196 | background-color: rgba(117, 117, 117, 0.8)
197 |
198 | .history
199 | transform: translateY(0)
200 |
201 | & + .results
202 | opacity: 0
--------------------------------------------------------------------------------
/res/css/_searchoptions.sass:
--------------------------------------------------------------------------------
1 | .search-options
2 | margin: 0 auto
3 | padding: 6px 0
4 | background: #fff
5 | box-shadow: 0 -1px 0 #e0e0e0, 0 0 2px rgba(0, 0, 0, .12), 0 2px 4px rgba(0, 0, 0, .24)
6 | max-width: 1072px
7 | overflow-x: scroll
8 | white-space: nowrap
--------------------------------------------------------------------------------
/res/css/_sendercolor.sass:
--------------------------------------------------------------------------------
1 | [data-sendercolor="0"]
2 | color: #e90d7f
3 |
4 | [data-sendercolor="1"]
5 | color: #8e55e9
6 |
7 | [data-sendercolor="2"]
8 | color: #b30e0e
9 |
10 | [data-sendercolor="3"]
11 | color: #17b339
12 |
13 | [data-sendercolor="4"]
14 | color: #58afb3
15 |
16 | [data-sendercolor="5"]
17 | color: #9d54b3
18 |
19 | [data-sendercolor="6"]
20 | color: #b39775
21 |
22 | [data-sendercolor="7"]
23 | color: #3176b3
24 |
25 | [data-sendercolor="8"]
26 | color: #e90d7f
27 |
28 | [data-sendercolor="9"]
29 | color: #8e55e9
30 |
31 | [data-sendercolor="a"]
32 | color: #b30e0e
33 |
34 | [data-sendercolor="b"]
35 | color: #17b339
36 |
37 | [data-sendercolor="c"]
38 | color: #58afb3
39 |
40 | [data-sendercolor="d"]
41 | color: #9d54b3
42 |
43 | [data-sendercolor="e"]
44 | color: #b39775
45 |
46 | [data-sendercolor="f"]
47 | color: #3176b3
--------------------------------------------------------------------------------
/res/css/_textfield.sass:
--------------------------------------------------------------------------------
1 | .textfield-floating-label
--------------------------------------------------------------------------------
/res/css/_util.sass:
--------------------------------------------------------------------------------
1 | @mixin vendor-prefix($name, $value)
2 | @each $vendor in ('-webkit-', '-moz-', '-ms-', '-o-', '')
3 | #{$vendor}#{$name}: #{$value}
--------------------------------------------------------------------------------
/res/css/login.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'Material Icons';
3 | font-style: normal;
4 | font-weight: 400;
5 | src: local("Material Icons"), local("MaterialIcons-Regular"), url(../icons/MaterialIcons-Regular.woff2) format("woff2"), url(../icons/MaterialIcons-Regular.woff) format("woff"); }
6 |
7 | .icon {
8 | font-family: 'Material Icons', sans-serif;
9 | font-weight: normal;
10 | font-style: normal;
11 | font-size: 24px;
12 | display: inline-block;
13 | width: 1em;
14 | height: 1em;
15 | line-height: 1;
16 | text-transform: none;
17 | letter-spacing: normal;
18 | word-wrap: normal;
19 | white-space: nowrap;
20 | direction: ltr;
21 | /* Support for all WebKit browsers. */
22 | -webkit-font-smoothing: antialiased;
23 | /* Support for Safari and Chrome. */
24 | text-rendering: optimizeLegibility;
25 | /* Support for Firefox. */
26 | -moz-osx-font-smoothing: grayscale;
27 | /* Support for IE. */
28 | font-feature-settings: 'liga'; }
29 |
30 | @font-face {
31 | font-family: 'Roboto';
32 | font-style: normal;
33 | font-weight: 400;
34 | src: local("Roboto"), local("Roboto-Regular"), url("../fonts/roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-regular.woff2") format("woff2"), url("../fonts/roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-regular.woff") format("woff"); }
35 |
36 | @font-face {
37 | font-family: 'Roboto';
38 | font-style: normal;
39 | font-weight: 700;
40 | src: local("Roboto Bold"), local("Roboto-Bold"), url("../fonts/roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-700.woff2") format("woff2"), url("../fonts/roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-700.woff") format("woff"); }
41 |
42 | * {
43 | padding: 0;
44 | margin: 0;
45 | box-sizing: border-box;
46 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); }
47 |
48 | html {
49 | background: #F2F2F2; }
50 |
51 | body {
52 | background: #F2F2F2;
53 | font-family: 'Roboto', sans-serif;
54 | font-size: 81.25%;
55 | display: flex;
56 | flex-direction: column;
57 | min-height: 100vh;
58 | position: relative;
59 | padding-right: .9375rem;
60 | padding-left: .9375rem; }
61 | body .header, body .footer {
62 | flex-grow: 1; }
63 |
64 | *:focus {
65 | outline: none; }
66 |
67 | ::-moz-focus-inner {
68 | border: 0; }
69 |
70 | form {
71 | background: #fff;
72 | max-width: 320px;
73 | width: 100%;
74 | padding: 20px 48px;
75 | margin: 0 auto;
76 | color: #212121;
77 | box-shadow: 0 -1px 0 #e5e5e5, 0 0 2px rgba(0, 0, 0, 0.12), 0 2px 4px rgba(0, 0, 0, 0.24); }
78 | @media (max-width: 320px) {
79 | form {
80 | padding: 8px 16px;
81 | margin: initial;
82 | width: initial;
83 | max-width: initial; } }
84 |
85 | .header {
86 | display: flex;
87 | flex-direction: column; }
88 | .header:before {
89 | content: "";
90 | flex-grow: 1; }
91 |
92 | h1 {
93 | color: #555;
94 | font-size: 32px;
95 | font-weight: 300;
96 | margin: 5px 0 15px;
97 | line-height: 1.5;
98 | text-align: center; }
99 |
100 | h2 {
101 | color: #555;
102 | font-size: 18px;
103 | font-weight: 400;
104 | margin: 5px 0 15px;
105 | line-height: 1.5;
106 | text-align: center; }
107 |
108 | input[type=text], input[type=password] {
109 | -webkit-appearance: none;
110 | -moz-appearance: none;
111 | -ms-appearance: none;
112 | -o-appearance: none;
113 | appearance: none;
114 | appearance: none;
115 | height: 36px;
116 | padding: 0 8px;
117 | background: #fff;
118 | border: 1px solid #d9d9d9;
119 | border-top: 1px solid #c0c0c0;
120 | box-sizing: border-box;
121 | border-radius: 1px;
122 | font-size: 15px;
123 | color: #404040;
124 | width: 100%;
125 | display: block;
126 | margin: 0 0 10px;
127 | z-index: 1;
128 | position: relative; }
129 |
130 | input[type="submit"] {
131 | display: inline-block;
132 | position: relative;
133 | border: 0 none;
134 | white-space: nowrap;
135 | text-align: center;
136 | font: inherit;
137 | text-transform: uppercase;
138 | text-decoration: none;
139 | outline: 0 none;
140 | border-radius: 3px;
141 | cursor: pointer;
142 | padding: 8px 16px;
143 | font-weight: 500;
144 | line-height: 20px;
145 | font-size: 14px;
146 | -webkit-user-select: none;
147 | -moz-user-select: none;
148 | -ms-user-select: none;
149 | -o-user-select: none;
150 | user-select: none;
151 | min-width: 88px;
152 | transition: background-size 500ms cubic-bezier(0.98, 0.005, 0.79, 1.005), background-color 500ms cubic-bezier(0.98, 0.005, 0.79, 1.005), color 500ms cubic-bezier(0.98, 0.005, 0.79, 1.005), box-shadow 300ms;
153 | background: #0a70c0 radial-gradient(circle, #105a94 10%, rgba(0, 0, 0, 0) 10.001%, rgba(0, 0, 0, 0)) no-repeat center center;
154 | background-size: 0;
155 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12), 0 1px 5px 0 rgba(0, 0, 0, 0.2);
156 | color: #fff;
157 | width: 100%;
158 | margin: 20px 0 15px; }
159 | input[type="submit"]:hover, input[type="submit"]:focus {
160 | background-color: #0a70c0;
161 | background-size: 1200%;
162 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.21), 0 3px 1px -2px rgba(0, 0, 0, 0.18), 0 1px 5px 0 rgba(0, 0, 0, 0.3); }
163 | input[type="submit"]:active {
164 | box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2); }
165 |
166 | form .message {
167 | margin: 0 0 1rem 0;
168 | font-size: 14px; }
169 |
170 | form .message.error {
171 | color: #b71c1c; }
172 |
--------------------------------------------------------------------------------
/res/css/login.css.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "mappings": "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",
4 | "sources": ["_icons.sass","_font.sass","login.sass"],
5 | "names": [],
6 | "file": "login.css"
7 | }
--------------------------------------------------------------------------------
/res/css/login.sass:
--------------------------------------------------------------------------------
1 | @import "util"
2 | @import "icons"
3 | @import "font"
4 |
5 | *
6 | padding: 0
7 | margin: 0
8 | box-sizing: border-box
9 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0)
10 |
11 | html
12 | background: #F2F2F2
13 |
14 | body
15 | background: #F2F2F2
16 | font-family: 'Roboto', sans-serif
17 | font-size: 81.25%
18 | display: flex
19 | flex-direction: column
20 | min-height: 100vh
21 | position: relative
22 | padding-right: .9375rem
23 | padding-left: .9375rem
24 |
25 | .header, .footer
26 | flex-grow: 1
27 |
28 | *:focus
29 | outline: none
30 |
31 | ::-moz-focus-inner
32 | border: 0
33 |
34 | form
35 | background: #fff
36 | max-width: 320px
37 | width: 100%
38 | padding: 20px 48px
39 | margin: 0 auto
40 | color: #212121
41 | box-shadow: 0 -1px 0 #e5e5e5, 0 0 2px rgba(0, 0, 0, .12), 0 2px 4px rgba(0, 0, 0, .24)
42 |
43 | @media(max-width: 320px)
44 | padding: 8px 16px
45 | margin: initial
46 | width: initial
47 | max-width: initial
48 |
49 | .header
50 | display: flex
51 | flex-direction: column
52 |
53 | &:before
54 | content: ""
55 | flex-grow: 1
56 |
57 | h1
58 | color: #555
59 | font-size: 32px
60 | font-weight: 300
61 | margin: 5px 0 15px
62 | line-height: 1.5
63 | text-align: center
64 |
65 | h2
66 | color: #555
67 | font-size: 18px
68 | font-weight: 400
69 | margin: 5px 0 15px
70 | line-height: 1.5
71 | text-align: center
72 |
73 | input[type=text], input[type=password]
74 | @include vendor-prefix('appearance', 'none')
75 | appearance: none
76 | height: 36px
77 | padding: 0 8px
78 | background: #fff
79 | border: 1px solid #d9d9d9
80 | border-top: 1px solid #c0c0c0
81 | box-sizing: border-box
82 | border-radius: 1px
83 | font-size: 15px
84 | color: #404040
85 | width: 100%
86 | display: block
87 | margin: 0 0 10px
88 | z-index: 1
89 | position: relative
90 |
91 | input[type="submit"]
92 | display: inline-block
93 | position: relative
94 | border: 0 none
95 | white-space: nowrap
96 | text-align: center
97 | font: inherit
98 | text-transform: uppercase
99 | text-decoration: none
100 | outline: 0 none
101 | border-radius: 3px
102 | cursor: pointer
103 | padding: 8px 16px
104 | font-weight: 500
105 | line-height: 20px
106 | font-size: 14px
107 | @include vendor-prefix('user-select', 'none')
108 | min-width: 88px
109 | transition: background-size 500ms cubic-bezier(0.98, 0.005, 0.79, 1.005), background-color 500ms cubic-bezier(0.98, 0.005, 0.79, 1.005), color 500ms cubic-bezier(0.98, 0.005, 0.79, 1.005), box-shadow 300ms
110 | background: #0a70c0 radial-gradient(circle, #105a94 10%, rgba(0, 0, 0, 0) 10.001%, rgba(0, 0, 0, 0)) no-repeat center center
111 | background-size: 0
112 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.12), 0 1px 5px 0 rgba(0, 0, 0, 0.2)
113 | color: #fff
114 | width: 100%
115 | margin: 20px 0 15px
116 |
117 | &:hover, &:focus
118 | background-color: #0a70c0
119 | background-size: 1200%
120 | box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.21), 0 3px 1px -2px rgba(0, 0, 0, 0.18), 0 1px 5px 0 rgba(0, 0, 0, 0.3)
121 |
122 | &:active
123 | box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.2)
124 |
125 | form .message
126 | margin: 0 0 1rem 0
127 | font-size: 14px
128 |
129 | form .message.error
130 | color: #b71c1c
--------------------------------------------------------------------------------
/res/css/search.css.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "mappings": "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",
4 | "sources": [
5 | "_icons.sass",
6 | "_font.sass",
7 | "search.sass",
8 | "_nav.sass",
9 | "_util.sass",
10 | "_searchoptions.sass",
11 | "_sendercolor.sass",
12 | "_mirccolor.sass",
13 | "_content.sass"
14 | ],
15 | "names": [],
16 | "file": "search.css"
17 | }
--------------------------------------------------------------------------------
/res/css/search.sass:
--------------------------------------------------------------------------------
1 | @import "util"
2 | @import "icons"
3 | @import "font"
4 |
5 | *
6 | padding: 0
7 | margin: 0
8 | box-sizing: border-box
9 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0)
10 |
11 | body
12 | background: #F2F2F2
13 | font-family: 'Roboto', sans-serif
14 | font-size: 81.25%
15 | padding-top: 56px
16 |
17 | *:focus
18 | outline: none
19 |
20 | ::-moz-focus-inner
21 | border: 0
22 |
23 | *.hidden
24 | visibility: hidden !important
25 | display: none !important
26 |
27 | @import "loading"
28 | @import "nav"
29 | @import "searchoptions"
30 | @import "content"
31 | @import "animations"
--------------------------------------------------------------------------------
/res/fonts/roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-700.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justjanne/quassel-rest-search/014d624e17bdf06fa1711db3238819fe095a77f8/res/fonts/roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-700.woff
--------------------------------------------------------------------------------
/res/fonts/roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-700.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justjanne/quassel-rest-search/014d624e17bdf06fa1711db3238819fe095a77f8/res/fonts/roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-700.woff2
--------------------------------------------------------------------------------
/res/fonts/roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justjanne/quassel-rest-search/014d624e17bdf06fa1711db3238819fe095a77f8/res/fonts/roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-regular.woff
--------------------------------------------------------------------------------
/res/fonts/roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justjanne/quassel-rest-search/014d624e17bdf06fa1711db3238819fe095a77f8/res/fonts/roboto-v15-latin-ext_cyrillic_greek-ext_cyrillic-ext_latin_greek_vietnamese-regular.woff2
--------------------------------------------------------------------------------
/res/icons/MaterialIcons-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justjanne/quassel-rest-search/014d624e17bdf06fa1711db3238819fe095a77f8/res/icons/MaterialIcons-Regular.woff
--------------------------------------------------------------------------------
/res/icons/MaterialIcons-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justjanne/quassel-rest-search/014d624e17bdf06fa1711db3238819fe095a77f8/res/icons/MaterialIcons-Regular.woff2
--------------------------------------------------------------------------------
/res/icons/chevron-down.svg:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/res/icons/chevron-up.svg:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/res/icons/dots-horizontal.svg:
--------------------------------------------------------------------------------
1 |
3 |
--------------------------------------------------------------------------------
/res/icons/error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/justjanne/quassel-rest-search/014d624e17bdf06fa1711db3238819fe095a77f8/res/icons/error.png
--------------------------------------------------------------------------------
/res/js/component/app.js:
--------------------------------------------------------------------------------
1 | const statehandler = new StateHandler();
2 | class App {
3 | constructor() {
4 | this.navigation = new Navigation();
5 | this.buffers = [];
6 | this.loadingQuery = 0;
7 | this.error = null;
8 | if (Storage.exists('language')) {
9 | moment.locale(Storage.get('language'));
10 | }
11 | this.render();
12 | this.navigation.addEventListener('search', query => {
13 | this.search(query);
14 | });
15 | statehandler.addEventListener('update', query => {
16 | this.search(query);
17 | });
18 | statehandler.init();
19 | }
20 | render() {
21 | this.elem = function () {
22 | var $$a = document.createElement('div');
23 | $$a.appendChildren(this.navigation.elem);
24 | $$a.appendChildren(this.resultContainer = function () {
25 | var $$d = document.createElement('div');
26 | $$d.setAttribute('class', 'results');
27 | return $$d;
28 | }.call(this));
29 | return $$a;
30 | }.call(this);
31 | this.buffers.forEach(buffer => this.insert(buffer));
32 | }
33 | search(query, sender, buffer, network, before, since) {
34 | this.clear();
35 | this.navigation.loading.show();
36 | this.navigation.input.blur();
37 | this.navigation.historyView.resetNavigation();
38 | this.navigation.historyView.add(new HistoryElement(query));
39 | this.navigation.input.value = query;
40 | statehandler.replace(query, sender, buffer, network, before, since);
41 | if (query.trim() === '')
42 | return;
43 | this.loadingQuery++;
44 | const queryId = this.loadingQuery;
45 | load('api/search/', statehandler.parse()).then(result => {
46 | if (this.loadingQuery !== queryId)
47 | return;
48 | this.navigation.loading.hide();
49 | this.buffers = result.map(buffer => {
50 | return new Buffer(buffer.bufferid, buffer.buffername, buffer.networkname, buffer.hasmore, buffer.messages.map(msg => {
51 | return new Context(new Message(msg.messageid, msg.type, msg.time, msg.sender, msg.message, true));
52 | }));
53 | });
54 | this.buffers.forEach(buffer => this.insert(buffer));
55 | if (this.buffers.length === 0) {
56 | this.showError(translation.error.none_found);
57 | }
58 | });
59 | }
60 | clear() {
61 | while (this.buffers.length) {
62 | const buffer = this.buffers.pop();
63 | this.resultContainer.removeChild(buffer.elem);
64 | }
65 | if (this.error) {
66 | this.resultContainer.removeChild(this.error.elem);
67 | this.error = null;
68 | }
69 | }
70 | clearAll() {
71 | this.clear();
72 | this.navigation.historyView.clear();
73 | statehandler.clear();
74 | }
75 | showError(text) {
76 | this.error = new Error(text);
77 | this.resultContainer.appendChild(this.error.elem);
78 | }
79 | insert(buffer) {
80 | this.resultContainer.appendChild(buffer.elem);
81 | buffer.addEventListener('loadMore', () => this.bufferLoadMore(buffer));
82 | buffer.addEventListener('loadBefore', context => {
83 | this.contextLoadBefore(buffer, context);
84 | });
85 | buffer.addEventListener('loadAfter', context => {
86 | this.contextLoadAfter(buffer, context);
87 | });
88 | buffer.addEventListener('loadInitial', context => {
89 | this.contextLoadInitial(buffer, context);
90 | });
91 | }
92 | bufferLoadMore(buffer) {
93 | if (buffer.loading)
94 | return;
95 | buffer.setLoading(true);
96 | const offset = buffer.count();
97 | console.log(offset);
98 | load('api/searchbuffer/', statehandler.parse({
99 | buffer: buffer.id,
100 | offset: offset
101 | })).then(result => {
102 | buffer.load(result);
103 | buffer.setLoading(false);
104 | });
105 | }
106 | contextLoadBefore(buffer, context) {
107 | if (context.loading)
108 | return;
109 | context.setLoading(true);
110 | load('api/backlog/', statehandler.parse({
111 | buffer: buffer.id,
112 | anchor: context.anchorBefore,
113 | after: 0,
114 | before: 10
115 | })).then(result => {
116 | context.loadBefore(result.map(msg => new Message(msg.messageid, msg.type, msg.time, msg.sender, msg.message)));
117 | context.setLoading(false);
118 | });
119 | }
120 | contextLoadAfter(buffer, context) {
121 | if (context.loading)
122 | return;
123 | context.setLoading(true);
124 | load('api/backlog/', statehandler.parse({
125 | buffer: buffer.id,
126 | anchor: context.anchorAfter,
127 | after: 10,
128 | before: 0
129 | })).then(result => {
130 | context.loadAfter(result.map(msg => new Message(msg.messageid, msg.type, msg.time, msg.sender, msg.message)));
131 | context.setLoading(false);
132 | });
133 | }
134 | contextLoadInitial(buffer, context) {
135 | if (context.loading)
136 | return;
137 | context.setLoading(true);
138 | load('api/backlog/', statehandler.parse({
139 | buffer: buffer.id,
140 | anchor: context.anchorAfter,
141 | after: 4,
142 | before: 4
143 | })).then(result => {
144 | const before = result.filter(msg => msg.messageid < context.anchorBefore);
145 | const after = result.filter(msg => msg.messageid > context.anchorAfter);
146 | context.loadBefore(before.map(msg => new Message(msg.messageid, msg.type, msg.time, msg.sender, msg.message)));
147 | context.loadAfter(after.map(msg => new Message(msg.messageid, msg.type, msg.time, msg.sender, msg.message)));
148 | context.setLoading(false);
149 | });
150 | }
151 | }
152 | const app = new App();
153 | document.body.insertBefore(app.elem, document.body.firstChild);
--------------------------------------------------------------------------------
/res/js/component/app.jsx:
--------------------------------------------------------------------------------
1 | const statehandler = new StateHandler();
2 |
3 | class App {
4 | constructor() {
5 | this.navigation = new Navigation();
6 | this.buffers = [];
7 |
8 | this.loadingQuery = 0;
9 | this.error = null;
10 |
11 | if (Storage.exists('language')) {
12 | moment.locale(Storage.get('language'));
13 | }
14 |
15 | this.render();
16 | this.navigation.addEventListener("search", (query) => {
17 | this.search(query)
18 | });
19 | statehandler.addEventListener("update", (query) => {
20 | this.search(query)
21 | });
22 | statehandler.init();
23 | }
24 |
25 | render() {
26 | this.elem =
27 | {this.navigation.elem}
28 | {this.resultContainer =
}
29 |
;
30 |
31 | this.buffers.forEach((buffer) => this.insert(buffer));
32 | }
33 |
34 | search(query, sender, buffer, network, before, since) {
35 | this.clear();
36 | this.navigation.loading.show();
37 |
38 | this.navigation.input.blur();
39 | this.navigation.historyView.resetNavigation();
40 | this.navigation.historyView.add(new HistoryElement(query));
41 | this.navigation.input.value = query;
42 | statehandler.replace(query, sender, buffer, network, before, since);
43 |
44 | if (query.trim() === "")
45 | return;
46 |
47 | this.loadingQuery++;
48 | const queryId = this.loadingQuery;
49 | load("api/search/", statehandler.parse()).then((result) => {
50 | if (this.loadingQuery !== queryId)
51 | return;
52 |
53 | this.navigation.loading.hide();
54 | this.buffers = result.map((buffer) => {
55 | return new Buffer(buffer.bufferid, buffer.buffername, buffer.networkname, buffer.hasmore, buffer.messages.map((msg) => {
56 | return new Context(new Message(msg.messageid, msg.type, msg.time, msg.sender, msg.message, true));
57 | }));
58 | });
59 | this.buffers.forEach((buffer) => this.insert(buffer));
60 | if (this.buffers.length === 0) {
61 | this.showError(translation.error.none_found);
62 | }
63 | });
64 | }
65 |
66 | clear() {
67 | while (this.buffers.length) {
68 | const buffer = this.buffers.pop();
69 | this.resultContainer.removeChild(buffer.elem);
70 | }
71 |
72 | if (this.error) {
73 | this.resultContainer.removeChild(this.error.elem);
74 | this.error = null;
75 | }
76 | }
77 |
78 | clearAll() {
79 | this.clear();
80 | this.navigation.historyView.clear();
81 | statehandler.clear();
82 | }
83 |
84 | showError(text) {
85 | this.error = new Error(text);
86 | this.resultContainer.appendChild(this.error.elem);
87 | }
88 |
89 | insert(buffer) {
90 | this.resultContainer.appendChild(buffer.elem);
91 | buffer.addEventListener("loadMore", () => this.bufferLoadMore(buffer));
92 | buffer.addEventListener("loadBefore", (context) => {
93 | this.contextLoadBefore(buffer, context);
94 | });
95 | buffer.addEventListener("loadAfter", (context) => {
96 | this.contextLoadAfter(buffer, context);
97 | });
98 | buffer.addEventListener("loadInitial", (context) => {
99 | this.contextLoadInitial(buffer, context);
100 | });
101 | }
102 |
103 | bufferLoadMore(buffer) {
104 | if (buffer.loading)
105 | return;
106 |
107 | buffer.setLoading(true);
108 | const offset = buffer.count();
109 | console.log(offset);
110 | load("api/searchbuffer/", statehandler.parse({buffer: buffer.id, offset: offset})).then((result) => {
111 | buffer.load(result);
112 | buffer.setLoading(false);
113 | });
114 | }
115 |
116 | contextLoadBefore(buffer, context) {
117 | if (context.loading )
118 | return;
119 |
120 | context.setLoading(true);
121 | load("api/backlog/", statehandler.parse({
122 | buffer: buffer.id,
123 | anchor: context.anchorBefore,
124 | after: 0,
125 | before: 10
126 | })).then((result) => {
127 | context.loadBefore(result.map((msg) => new Message(msg.messageid, msg.type, msg.time, msg.sender, msg.message)));
128 | context.setLoading(false);
129 | });
130 | }
131 |
132 | contextLoadAfter(buffer, context) {
133 | if (context.loading)
134 | return;
135 |
136 | context.setLoading(true);
137 | load("api/backlog/", statehandler.parse({
138 | buffer: buffer.id,
139 | anchor: context.anchorAfter,
140 | after: 10,
141 | before: 0
142 | })).then((result) => {
143 | context.loadAfter(result.map((msg) => new Message(msg.messageid, msg.type, msg.time, msg.sender, msg.message)));
144 | context.setLoading(false);
145 | });
146 | }
147 |
148 | contextLoadInitial(buffer, context) {
149 | if (context.loading)
150 | return;
151 |
152 | context.setLoading(true);
153 | load("api/backlog/", statehandler.parse({
154 | buffer: buffer.id,
155 | anchor: context.anchorAfter,
156 | after: 4,
157 | before: 4
158 | })).then((result) => {
159 | const before = result.filter((msg) => msg.messageid < context.anchorBefore);
160 | const after = result.filter((msg) => msg.messageid > context.anchorAfter);
161 |
162 | context.loadBefore(before.map((msg) => new Message(msg.messageid, msg.type, msg.time, msg.sender, msg.message)));
163 | context.loadAfter(after.map((msg) => new Message(msg.messageid, msg.type, msg.time, msg.sender, msg.message)));
164 | context.setLoading(false);
165 | });
166 | }
167 | }
168 |
169 | const app = new App();
170 | document.body.insertBefore(app.elem, document.body.firstChild);
171 |
--------------------------------------------------------------------------------
/res/js/component/buffer.js:
--------------------------------------------------------------------------------
1 | class Buffer extends Component {
2 | constructor(id, name, network, hasMore, contextList = []) {
3 | super();
4 | this.id = id;
5 | this.name = name;
6 | this.network = network;
7 | this.contextList = contextList;
8 | this.render();
9 | this.contextList.forEach(context => this.insert(context));
10 | this.hasMore = hasMore;
11 | this.loading = false;
12 | this.neverLoaded = true;
13 | this.loadMoreBtn.setVisible(hasMore);
14 | }
15 | render() {
16 | this.elem = function () {
17 | var $$a = document.createElement('div');
18 | $$a.setAttribute('class', 'buffer');
19 | var $$b = document.createElement('div');
20 | $$b.setAttribute('class', 'title');
21 | $$a.appendChild($$b);
22 | var $$c = document.createElement('h2');
23 | $$b.appendChild($$c);
24 | $$c.appendChildren(this.network);
25 | var $$e = document.createTextNode(' \u2013 ');
26 | $$c.appendChild($$e);
27 | $$c.appendChildren(this.name);
28 | var $$g = document.createElement('button');
29 | $$g.addEventListener('click', () => this.focus());
30 | $$b.appendChild($$g);
31 | var $$h = document.createElement('span');
32 | $$h.setAttribute('class', 'close');
33 | $$g.appendChild($$h);
34 | $$h.appendChildren(translation.buffer.close);
35 | var $$j = document.createElement('div');
36 | $$j.setAttribute('class', 'container');
37 | $$a.appendChild($$j);
38 | $$j.appendChildren(this.insertContainerFirst = function () {
39 | var $$l = document.createElement('div');
40 | $$l.setAttribute('class', 'primary');
41 | return $$l;
42 | }.call(this));
43 | $$j.appendChildren(this.insertContainer = function () {
44 | var $$n = document.createElement('div');
45 | $$n.setAttribute('class', 'secondary');
46 | return $$n;
47 | }.call(this));
48 | $$j.appendChildren((this.loadMoreBtn = new LoadMore(translation.results.show_more)).elem);
49 | return $$a;
50 | }.call(this);
51 | this.loadMoreBtn.addEventListener('click', () => this.loadMore());
52 | return this.elem;
53 | }
54 | count() {
55 | return this.contextList.length;
56 | }
57 | loadMore() {
58 | if (this.elem.classList.contains('focus') || this.hasMore && this.neverLoaded) {
59 | this.sendEvent('loadMore', []);
60 | }
61 | this.focus(true);
62 | }
63 | focus(focus) {
64 | if (focus === undefined)
65 | focus = !this.elem.classList.contains('focus');
66 | this.elem.classList.toggle('focus', focus);
67 | this.sendEvent('focus', focus);
68 | if (focus === false) {
69 | const bottomVisible = this.elem.offsetTop - this.insertContainerFirst.offsetTop + 20 + this.insertContainerFirst.offsetHeight;
70 | const fullyVisible = this.elem.offsetTop - this.insertContainerFirst.offsetTop + 20;
71 | const targetPosition = window.scrollY - this.insertContainer.offsetHeight;
72 | window.scrollTo(0, targetPosition > bottomVisible - 56 ? fullyVisible : targetPosition);
73 | }
74 | }
75 | load(resultSet) {
76 | resultSet.results.map(msg => new Context(new Message(msg.messageid, msg.type, msg.time, msg.sender, msg.message, true))).forEach(context => {
77 | this.contextList.push(context);
78 | this.insert(context);
79 | });
80 | this.hasMore = resultSet.hasmore;
81 | this.elem.classList.toggle('hasmore', this.hasMore);
82 | }
83 | insert(context) {
84 | let container = this.insertContainerFirst.childElementCount < 4 ? this.insertContainerFirst : this.insertContainer;
85 | container.appendChild(context.elem);
86 | context.addEventListener('loadBefore', context => {
87 | this.sendEvent('loadBefore', [context]);
88 | });
89 | context.addEventListener('loadAfter', context => {
90 | this.sendEvent('loadAfter', [context]);
91 | });
92 | context.addEventListener('loadInitial', context => {
93 | this.sendEvent('loadInitial', [context]);
94 | });
95 | this.neverLoaded = false;
96 | }
97 | setLoading(value) {
98 | this.loading = value;
99 | }
100 | }
--------------------------------------------------------------------------------
/res/js/component/buffer.jsx:
--------------------------------------------------------------------------------
1 | class Buffer extends Component {
2 | constructor(id, name, network, hasMore, contextList = []) {
3 | super();
4 |
5 | this.id = id;
6 | this.name = name;
7 | this.network = network;
8 | this.contextList = contextList;
9 |
10 | this.render();
11 | this.contextList.forEach((context) => this.insert(context));
12 |
13 | this.hasMore = hasMore;
14 | this.loading = false;
15 | this.neverLoaded = true;
16 |
17 | this.loadMoreBtn.setVisible(hasMore);
18 | }
19 |
20 | render() {
21 | this.elem = (
22 |
23 |
24 |
{this.network} – {this.name}
25 |
28 |
29 |
30 | {this.insertContainerFirst = (
31 |
32 | )}
33 | {this.insertContainer = (
34 |
35 |
36 | )}
37 | {(this.loadMoreBtn = new LoadMore(translation.results.show_more)).elem}
38 |
39 |
40 | );
41 | this.loadMoreBtn.addEventListener("click", () => this.loadMore());
42 | return this.elem;
43 | }
44 |
45 | count() {
46 | return this.contextList.length
47 | }
48 |
49 | loadMore() {
50 | if (this.elem.classList.contains("focus") || (this.hasMore && this.neverLoaded)) {
51 | this.sendEvent("loadMore", []);
52 | }
53 | this.focus(true);
54 | }
55 |
56 | focus(focus) {
57 | if (focus === undefined)
58 | focus = !this.elem.classList.contains("focus");
59 |
60 | this.elem.classList.toggle("focus", focus);
61 | this.sendEvent("focus", focus);
62 |
63 | if (focus === false) {
64 | const bottomVisible = this.elem.offsetTop - this.insertContainerFirst.offsetTop + 20 + this.insertContainerFirst.offsetHeight;
65 | const fullyVisible = this.elem.offsetTop - this.insertContainerFirst.offsetTop + 20;
66 | const targetPosition = window.scrollY - this.insertContainer.offsetHeight;
67 | window.scrollTo(0, (targetPosition > bottomVisible - 56) ? fullyVisible : targetPosition);
68 | }
69 | }
70 |
71 | load(resultSet) {
72 | resultSet.results
73 | .map((msg) => new Context(new Message(msg.messageid, msg.type, msg.time, msg.sender, msg.message, true)))
74 | .forEach((context) => {
75 | this.contextList.push(context);
76 | this.insert(context)
77 | });
78 | this.hasMore = resultSet.hasmore;
79 | this.elem.classList.toggle('hasmore', this.hasMore);
80 | }
81 |
82 | insert(context) {
83 | let container = (this.insertContainerFirst.childElementCount < 4 ? this.insertContainerFirst : this.insertContainer);
84 | container.appendChild(context.elem);
85 | context.addEventListener("loadBefore", (context) => {
86 | this.sendEvent("loadBefore", [context])
87 | });
88 | context.addEventListener("loadAfter", (context) => {
89 | this.sendEvent("loadAfter", [context])
90 | });
91 | context.addEventListener("loadInitial", (context) => {
92 | this.sendEvent("loadInitial", [context])
93 | });
94 | this.neverLoaded = false;
95 | }
96 |
97 | setLoading(value) {
98 | // Add UI indicator
99 | this.loading = value;
100 | }
101 | }
--------------------------------------------------------------------------------
/res/js/component/context.js:
--------------------------------------------------------------------------------
1 | class Context extends Component {
2 | constructor(message, beforeList = [], afterList = []) {
3 | super();
4 | this.message = message;
5 | this.beforeList = beforeList;
6 | this.afterList = afterList;
7 | this.render();
8 | this.insertAfterTarget = this.loadAfterBtn.elem;
9 | this.beforeList.forEach(it => this.insertBefore(it));
10 | this.afterList.forEach(it => this.insertAfter(it));
11 | this.message.addEventListener('focus', () => this.focus());
12 | this.anchorBefore = this.message.id;
13 | this.anchorAfter = this.message.id;
14 | this.loading = false;
15 | }
16 | render() {
17 | return this.elem = function () {
18 | var $$a = document.createElement('span');
19 | $$a.setAttribute('class', 'context');
20 | $$a.appendChildren(this.containerBefore = function () {
21 | var $$c = document.createElement('span');
22 | $$c.setAttribute('class', 'container before');
23 | $$c.appendChildren((this.loadBeforeBtn = new LoadMore(translation.context.load_earlier, () => this.triggerLoadBefore())).elem);
24 | return $$c;
25 | }.call(this));
26 | $$a.appendChildren(this.message.elem);
27 | $$a.appendChildren(this.containerAfter = function () {
28 | var $$g = document.createElement('span');
29 | $$g.setAttribute('class', 'container after');
30 | $$g.appendChildren((this.loadAfterBtn = new LoadMore(translation.context.load_later, () => this.triggerLoadAfter())).elem);
31 | return $$g;
32 | }.call(this));
33 | return $$a;
34 | }.call(this);
35 | }
36 | focus(focus) {
37 | if (focus === undefined)
38 | focus = !this.elem.classList.contains('focus');
39 | if (this.anchorBefore === this.message.id && this.anchorAfter === this.message.id) {
40 | this.triggerLoadInitial();
41 | }
42 | this.elem.classList.toggle('focus', focus);
43 | this.sendEvent('focus', focus);
44 | }
45 | insertBefore(message) {
46 | this.containerBefore.insertBefore(message.elem, this.insertBeforeTarget);
47 | this.insertBeforeTarget = message.elem;
48 | this.anchorBefore = message.id;
49 | }
50 | insertAfter(message) {
51 | this.containerAfter.insertBefore(message.elem, this.insertAfterTarget);
52 | this.anchorAfter = message.id;
53 | }
54 | triggerLoadBefore() {
55 | this.sendEvent('loadBefore', [this]);
56 | }
57 | triggerLoadAfter() {
58 | this.sendEvent('loadAfter', [this]);
59 | }
60 | triggerLoadInitial() {
61 | this.sendEvent('loadInitial', [this]);
62 | }
63 | loadBefore(elements) {
64 | this.beforeList = elements.concat(this.beforeList);
65 | elements.forEach(it => this.insertBefore(it));
66 | }
67 | loadAfter(elements) {
68 | this.afterList = elements.concat(this.afterList);
69 | elements.forEach(it => this.insertAfter(it));
70 | }
71 | setLoading(value) {
72 | this.loading = value;
73 | }
74 | }
--------------------------------------------------------------------------------
/res/js/component/context.jsx:
--------------------------------------------------------------------------------
1 | class Context extends Component {
2 | constructor(message, beforeList = [], afterList = []) {
3 | super();
4 |
5 | this.message = message;
6 | this.beforeList = beforeList;
7 | this.afterList = afterList;
8 |
9 | this.render();
10 | this.insertAfterTarget = this.loadAfterBtn.elem;
11 | this.beforeList.forEach((it) => this.insertBefore(it));
12 | this.afterList.forEach((it) => this.insertAfter(it));
13 |
14 | this.message.addEventListener("focus", () => this.focus());
15 |
16 | this.anchorBefore = this.message.id;
17 | this.anchorAfter = this.message.id;
18 |
19 | this.loading = false;
20 | }
21 |
22 | render() {
23 | return this.elem = (
24 |
25 | {this.containerBefore = (
26 |
27 | {(this.loadBeforeBtn = new LoadMore(translation.context.load_earlier, () => this.triggerLoadBefore())).elem}
28 |
29 | )}
30 | {this.message.elem}
31 | {this.containerAfter = (
32 |
33 | {(this.loadAfterBtn = new LoadMore(translation.context.load_later, () => this.triggerLoadAfter())).elem}
34 |
35 | )}
36 |
37 | );
38 | }
39 |
40 | focus(focus) {
41 | if (focus === undefined)
42 | focus = !this.elem.classList.contains("focus");
43 |
44 | if (this.anchorBefore === this.message.id && this.anchorAfter === this.message.id) {
45 | this.triggerLoadInitial();
46 | }
47 |
48 | this.elem.classList.toggle("focus", focus);
49 | this.sendEvent("focus", focus);
50 | }
51 |
52 | insertBefore(message) {
53 | this.containerBefore.insertBefore(message.elem, this.insertBeforeTarget);
54 | this.insertBeforeTarget = message.elem;
55 | this.anchorBefore = message.id;
56 | }
57 |
58 | insertAfter(message) {
59 | this.containerAfter.insertBefore(message.elem, this.insertAfterTarget);
60 | this.anchorAfter = message.id;
61 | }
62 |
63 | triggerLoadBefore() {
64 | this.sendEvent("loadBefore", [this]);
65 | }
66 |
67 | triggerLoadAfter() {
68 | this.sendEvent("loadAfter", [this]);
69 | }
70 |
71 | triggerLoadInitial() {
72 | this.sendEvent("loadInitial", [this]);
73 | }
74 |
75 | loadBefore(elements) {
76 | this.beforeList = elements.concat(this.beforeList);
77 | elements.forEach((it) => this.insertBefore(it));
78 | }
79 |
80 | loadAfter(elements) {
81 | this.afterList = elements.concat(this.afterList);
82 | elements.forEach((it) => this.insertAfter(it));
83 | }
84 |
85 | setLoading(value) {
86 | // Add UI indicator
87 | this.loading = value;
88 | }
89 | }
--------------------------------------------------------------------------------
/res/js/component/error.js:
--------------------------------------------------------------------------------
1 | class Error {
2 | constructor(text) {
3 | this.render(text);
4 | }
5 | render(text) {
6 | return this.elem = function () {
7 | var $$a = document.createElement('div');
8 | $$a.setAttribute('class', 'error');
9 | var $$b = document.createElement('img');
10 | $$b.setAttribute('src', 'res/icons/error.png');
11 | $$a.appendChild($$b);
12 | var $$c = document.createElement('h1');
13 | $$a.appendChild($$c);
14 | $$c.appendChildren(text);
15 | return $$a;
16 | }.call(this);
17 | }
18 | }
--------------------------------------------------------------------------------
/res/js/component/error.jsx:
--------------------------------------------------------------------------------
1 | class Error {
2 | constructor(text) {
3 | this.render(text);
4 | }
5 |
6 | render(text) {
7 | return this.elem = (
8 |
9 |

10 |
{text}
11 |
12 | );
13 | }
14 | }
--------------------------------------------------------------------------------
/res/js/component/history.js:
--------------------------------------------------------------------------------
1 | const HISTORY_KEY = 'history';
2 | const HISTORY_MAX_LENGTH = 4;
3 | class HistoryView {
4 | constructor() {
5 | this.index = -1;
6 | this.elements = this.load().map(function (query) {
7 | return new HistoryElement(query);
8 | });
9 | this.render();
10 | this.insert(this.elements);
11 | }
12 | render() {
13 | return this.elem = function () {
14 | var $$a = document.createElement('div');
15 | $$a.setAttribute('class', 'history');
16 | $$a.appendChildren(this.list = function () {
17 | var $$c = document.createElement('ul');
18 | $$c.appendChildren((this.noHistory = new NoHistoryElement()).elem);
19 | return $$c;
20 | }.call(this));
21 | return $$a;
22 | }.call(this);
23 | }
24 | insert(items) {
25 | if (!(items instanceof Array))
26 | return this.insert([items]);
27 | const anchor = this.list.firstChild;
28 | items.forEach(item => this.list.insertBefore(item.elem, anchor));
29 | if (items.length && this.noHistory.elem.parentNode === this.list)
30 | this.list.removeChild(this.noHistory.elem);
31 | }
32 | add(item) {
33 | if (item.query === '')
34 | return;
35 | const idx = this.elements.map(item => item.query).indexOf(item.query);
36 | if (idx !== -1) {
37 | this.list.removeChild(this.elements[idx].elem);
38 | this.elements.splice(idx, 1);
39 | }
40 | this.elements.unshift(item);
41 | this.insert(item);
42 | this.truncate();
43 | this.store();
44 | }
45 | clear() {
46 | while (this.elements.length) {
47 | this.list.removeChild(this.elements.pop().elem);
48 | }
49 | this.store();
50 | this.list.appendChild(this.noHistory.elem);
51 | }
52 | load() {
53 | const loaded = localStorage[HISTORY_KEY];
54 | return JSON.parse(loaded === undefined ? '[]' : loaded);
55 | }
56 | store() {
57 | localStorage[HISTORY_KEY] = JSON.stringify(this.elements.map(item => item.query));
58 | }
59 | navigateBefore() {
60 | if (this.elements[this.index])
61 | this.elements[this.index].focus(false);
62 | this.index++;
63 | this.index %= this.elements.length;
64 | if (this.elements[this.index])
65 | this.elements[this.index].focus(true);
66 | }
67 | navigateLater() {
68 | if (this.elements[this.index])
69 | this.elements[this.index].focus(false);
70 | this.index--;
71 | if (this.index < 0)
72 | this.index = -1;
73 | else
74 | this.index %= this.elements.length;
75 | if (this.elements[this.index])
76 | this.elements[this.index].focus(true);
77 | }
78 | resetNavigation() {
79 | if (this.elements[this.index])
80 | this.elements[this.index].focus(false);
81 | this.index = -1;
82 | }
83 | truncate() {
84 | while (this.elements.length > HISTORY_MAX_LENGTH)
85 | this.list.removeChild(this.elements.pop().elem);
86 | }
87 | }
--------------------------------------------------------------------------------
/res/js/component/history.jsx:
--------------------------------------------------------------------------------
1 | const HISTORY_KEY = "history";
2 | const HISTORY_MAX_LENGTH = 4;
3 |
4 | class HistoryView {
5 | constructor() {
6 | this.index = -1;
7 |
8 | this.elements = this.load().map(function (query) {
9 | return new HistoryElement(query);
10 | });
11 |
12 | this.render();
13 | this.insert(this.elements);
14 | }
15 |
16 | render() {
17 | return this.elem = (
18 |
19 | {this.list = (
20 |
21 | {(this.noHistory = new NoHistoryElement()).elem}
22 |
23 | )}
24 |
25 | );
26 | }
27 |
28 | insert(items) {
29 | if (!(items instanceof Array))
30 | return this.insert([items]);
31 |
32 | const anchor = this.list.firstChild;
33 | items.forEach(item => this.list.insertBefore(item.elem, anchor));
34 | if (items.length && this.noHistory.elem.parentNode === this.list)
35 | this.list.removeChild(this.noHistory.elem);
36 | }
37 |
38 | add(item) {
39 | if (item.query === "")
40 | return;
41 |
42 | const idx = this.elements.map((item) => item.query).indexOf(item.query);
43 | if (idx !== -1) {
44 | this.list.removeChild(this.elements[idx].elem);
45 | this.elements.splice(idx, 1);
46 | }
47 |
48 | this.elements.unshift(item);
49 | this.insert(item);
50 |
51 | this.truncate();
52 |
53 | this.store();
54 | }
55 |
56 | clear() {
57 | while (this.elements.length) {
58 | this.list.removeChild(this.elements.pop().elem);
59 | }
60 | this.store();
61 | this.list.appendChild(this.noHistory.elem);
62 | }
63 |
64 | load() {
65 | const loaded = localStorage[HISTORY_KEY];
66 | return JSON.parse(loaded === undefined ? "[]" : loaded);
67 | }
68 |
69 | store() {
70 | localStorage[HISTORY_KEY] = JSON.stringify(this.elements.map((item) => item.query));
71 | }
72 |
73 | navigateBefore() {
74 | if (this.elements[this.index])
75 | this.elements[this.index].focus(false);
76 |
77 | this.index++;
78 | this.index %= this.elements.length;
79 |
80 | if (this.elements[this.index])
81 | this.elements[this.index].focus(true);
82 | }
83 |
84 | navigateLater() {
85 | if (this.elements[this.index])
86 | this.elements[this.index].focus(false);
87 |
88 | this.index--;
89 | if (this.index < 0)
90 | this.index = -1;
91 | else
92 | this.index %= this.elements.length;
93 |
94 | if (this.elements[this.index])
95 | this.elements[this.index].focus(true);
96 | }
97 |
98 | resetNavigation() {
99 | if (this.elements[this.index])
100 | this.elements[this.index].focus(false);
101 |
102 | this.index = -1;
103 | }
104 |
105 | truncate() {
106 | while (this.elements.length > HISTORY_MAX_LENGTH)
107 | this.list.removeChild(this.elements.pop().elem);
108 | }
109 | }
--------------------------------------------------------------------------------
/res/js/component/historyelement.js:
--------------------------------------------------------------------------------
1 | class HistoryElement {
2 | constructor(query) {
3 | this.query = query;
4 | this.render();
5 | this.elem.addEventListener('mousedown', event => {
6 | if (event.buttons === 0 || event.buttons === 1) {
7 | statehandler.replace(this.query);
8 | event.preventDefault();
9 | }
10 | });
11 | }
12 | render() {
13 | return this.elem = function () {
14 | var $$a = document.createElement('li');
15 | var $$b = document.createElement('span');
16 | $$b.setAttribute('class', 'icon');
17 | $$a.appendChild($$b);
18 | var $$c = document.createTextNode('history');
19 | $$b.appendChild($$c);
20 | $$a.appendChildren(this.query);
21 | return $$a;
22 | }.call(this);
23 | }
24 | focus(focus) {
25 | this.elem.classList.toggle('focus', focus);
26 | }
27 | }
--------------------------------------------------------------------------------
/res/js/component/historyelement.jsx:
--------------------------------------------------------------------------------
1 | class HistoryElement {
2 | constructor(query) {
3 | this.query = query;
4 |
5 | this.render();
6 | this.elem.addEventListener("mousedown", (event) => {
7 | if (event.buttons === 0 || event.buttons === 1) {
8 | statehandler.replace(this.query);
9 | event.preventDefault();
10 | }
11 | });
12 | }
13 |
14 | render() {
15 | return this.elem = (
16 |
17 | history
18 | {this.query}
19 |
20 | );
21 | }
22 |
23 | focus(focus) {
24 | this.elem.classList.toggle("focus", focus);
25 | }
26 | }
--------------------------------------------------------------------------------
/res/js/component/loading.js:
--------------------------------------------------------------------------------
1 | class Loading {
2 | constructor() {
3 | this.render();
4 | }
5 | render() {
6 | return this.elem = function () {
7 | var $$a = document.createElement('div');
8 | $$a.setAttribute('class', 'progress');
9 | var $$b = document.createElement('div');
10 | $$b.setAttribute('class', 'indeterminate');
11 | $$a.appendChild($$b);
12 | return $$a;
13 | }.call(this);
14 | }
15 | show() {
16 | this.elem.classList.add('visible');
17 | }
18 | hide() {
19 | this.elem.classList.remove('visible');
20 | }
21 | }
--------------------------------------------------------------------------------
/res/js/component/loading.jsx:
--------------------------------------------------------------------------------
1 | class Loading {
2 | constructor() {
3 | this.render();
4 | }
5 |
6 | render() {
7 | return this.elem = (
8 |
12 | );
13 | }
14 |
15 | show() {
16 | this.elem.classList.add("visible");
17 | }
18 |
19 | hide() {
20 | this.elem.classList.remove("visible");
21 | }
22 | }
--------------------------------------------------------------------------------
/res/js/component/loadmore.js:
--------------------------------------------------------------------------------
1 | class LoadMore extends Component {
2 | constructor(text, eventListener) {
3 | super();
4 | this.render(text);
5 | if (eventListener)
6 | this.addEventListener('click', eventListener);
7 | }
8 | render(text) {
9 | return this.elem = function () {
10 | var $$a = document.createElement('div');
11 | $$a.setAttribute('class', 'inline-button');
12 | $$a.addEventListener('click', event => this.sendEvent('click', [event]));
13 | $$a.appendChildren(text);
14 | return $$a;
15 | }.call(this);
16 | }
17 | }
--------------------------------------------------------------------------------
/res/js/component/loadmore.jsx:
--------------------------------------------------------------------------------
1 | class LoadMore extends Component {
2 | constructor(text, eventListener) {
3 | super();
4 | this.render(text);
5 | if (eventListener)
6 | this.addEventListener("click", eventListener);
7 | }
8 |
9 | render(text) {
10 | return this.elem = (
11 | this.sendEvent("click", [event])}>
12 | {text}
13 |
14 | );
15 | }
16 | }
--------------------------------------------------------------------------------
/res/js/component/message.js:
--------------------------------------------------------------------------------
1 | class Message extends Component {
2 | constructor(id, type, time, sender, content, isAnchor) {
3 | super();
4 | this.id = id;
5 | this.type = type;
6 | this.time = time.replace(' ', 'T') + 'Z';
7 | this.sender = sender;
8 | this.content = content;
9 | this.isAnchor = isAnchor;
10 | this.render();
11 | }
12 | render() {
13 | const classes = ['message'];
14 | if ((this.type & 2) !== 0)
15 | classes.push('notice');
16 | if ((this.type & 4) !== 0)
17 | classes.push('action');
18 | const content = MircColorHandler.render(this.content);
19 | return this.elem = function () {
20 | var $$a = document.createElement('span');
21 | $$a.setAttribute('class', classes.join(' '));
22 | var $$b = document.createElement('span');
23 | $$a.appendChild($$b);
24 | var $$c = document.createElement('span');
25 | $$c.setAttribute('class', 'invisible');
26 | $$c.setAttribute('aria-hidden', 'true');
27 | $$b.appendChild($$c);
28 | var $$d = document.createTextNode('[');
29 | $$c.appendChild($$d);
30 | var $$e = document.createElement('time');
31 | $$e.setAttribute('dateTime', this.time);
32 | $$b.appendChild($$e);
33 | $$e.appendChildren(this.formatTime());
34 | var $$g = document.createElement('span');
35 | $$g.setAttribute('class', 'invisible');
36 | $$g.setAttribute('aria-hidden', 'true');
37 | $$b.appendChild($$g);
38 | var $$h = document.createTextNode(']');
39 | $$g.appendChild($$h);
40 | var $$i = document.createElement('span');
41 | $$i.setAttribute('class', 'container');
42 | $$a.appendChild($$i);
43 | var $$j = document.createElement('span');
44 | $$j.setAttribute('class', 'sender');
45 | $$j.setAttribute('data-sendercolor', SenderColorHandler.nickToColor(this.getNick()));
46 | $$i.appendChild($$j);
47 | var $$k = document.createElement('span');
48 | $$k.setAttribute('class', 'invisible');
49 | $$k.setAttribute('aria-hidden', 'true');
50 | $$j.appendChild($$k);
51 | var $$l = document.createTextNode(' <');
52 | $$k.appendChild($$l);
53 | $$j.appendChildren(this.getNick());
54 | var $$n = document.createElement('span');
55 | $$n.setAttribute('class', 'invisible');
56 | $$n.setAttribute('aria-hidden', 'true');
57 | $$j.appendChild($$n);
58 | var $$o = document.createTextNode('> ');
59 | $$n.appendChild($$o);
60 | var $$p = document.createElement('span');
61 | $$p.setAttribute('class', 'content');
62 | $$i.appendChild($$p);
63 | $$p.appendChildren(content.length ? content : null);
64 | $$a.appendChildren(this.isAnchor ? function () {
65 | var $$s = document.createElement('a');
66 | $$s.setAttribute('class', 'more icon');
67 | $$s.setAttribute('role', 'button');
68 | $$s.setAttribute('aria-label', translation.context.show_hide);
69 | $$s.addEventListener('click', () => this.sendEvent('focus', []));
70 | var $$t = document.createTextNode('list');
71 | $$s.appendChild($$t);
72 | return $$s;
73 | }.call(this) : null);
74 | var $$u = document.createElement('br');
75 | $$a.appendChild($$u);
76 | return $$a;
77 | }.call(this);
78 | }
79 | formatTime() {
80 | const dateFormat = Storage.exists('dateformat') ? Storage.get('dateformat') : 'L';
81 | const timeFormat = Storage.exists('timeformat') ? Storage.get('timeformat') : 'LT';
82 | const dateTimeFormat = dateFormat + ' ' + timeFormat;
83 | return moment(new Date(this.time)).format(dateTimeFormat);
84 | }
85 | getNick() {
86 | return this.sender.split('!')[0];
87 | }
88 | getIdent() {
89 | return this.sender.split('@')[0].split('!')[1];
90 | }
91 | getHost() {
92 | return this.sender.split('@')[1];
93 | }
94 | }
--------------------------------------------------------------------------------
/res/js/component/message.jsx:
--------------------------------------------------------------------------------
1 | class Message extends Component {
2 | constructor(id, type, time, sender, content, isAnchor) {
3 | super();
4 |
5 | this.id = id;
6 | this.type = type;
7 | this.time = time.replace(" ", "T") + "Z";
8 | this.sender = sender;
9 | this.content = content;
10 | this.isAnchor = isAnchor;
11 |
12 | this.render();
13 | }
14 |
15 | render() {
16 | const classes = ["message"];
17 | if ((this.type & 0x00000002) !== 0)
18 | classes.push("notice");
19 | if ((this.type & 0x00000004) !== 0)
20 | classes.push("action");
21 |
22 | const content = MircColorHandler.render(this.content);
23 |
24 | return this.elem = (
25 |
26 |
27 | [
28 |
29 | ]
30 |
31 |
32 |
33 | <
34 | {this.getNick()}
35 | >
36 |
37 |
38 | {content.length ? content : null}
39 |
40 |
41 | {this.isAnchor ? (
42 | this.sendEvent("focus", [])}>list
43 | ) : null}
44 |
45 |
46 | );
47 | }
48 |
49 | formatTime() {
50 | const dateFormat = Storage.exists('dateformat') ? Storage.get('dateformat') : 'L';
51 | const timeFormat = Storage.exists('timeformat') ? Storage.get('timeformat') : 'LT';
52 | const dateTimeFormat = dateFormat + " " + timeFormat;
53 |
54 | return moment(new Date(this.time)).format(dateTimeFormat);
55 | }
56 |
57 | getNick() {
58 | return this.sender.split("!")[0];
59 | }
60 |
61 | getIdent() {
62 | return this.sender.split("@")[0].split("!")[1];
63 | }
64 |
65 | getHost() {
66 | return this.sender.split("@")[1];
67 | }
68 | }
--------------------------------------------------------------------------------
/res/js/component/nav.js:
--------------------------------------------------------------------------------
1 | const keyMapping = {
2 | 13: 'Enter',
3 | 27: 'Escape',
4 | 38: 'ArrowUp',
5 | 40: 'ArrowDown'
6 | };
7 | class Navigation extends Component {
8 | constructor() {
9 | super();
10 | this.render();
11 | }
12 | render() {
13 | return this.elem = function () {
14 | var $$a = document.createElement('div');
15 | $$a.setAttribute('class', 'nav');
16 | var $$b = document.createElement('div');
17 | $$b.setAttribute('class', 'bar');
18 | $$a.appendChild($$b);
19 | var $$c = document.createElement('div');
20 | $$c.setAttribute('class', 'container');
21 | $$b.appendChild($$c);
22 | var $$d = document.createElement('div');
23 | $$d.setAttribute('class', 'searchBar');
24 | $$c.appendChild($$d);
25 | var $$e = document.createElement('p');
26 | $$e.setAttribute('class', 'icon');
27 | $$d.appendChild($$e);
28 | var $$f = document.createTextNode('search');
29 | $$e.appendChild($$f);
30 | $$d.appendChildren(this.input = function () {
31 | var $$h = document.createElement('input');
32 | $$h.setAttribute('class', 'search');
33 | $$h.setAttribute('placeholder', translation.search);
34 | $$h.setAttribute('type', 'search');
35 | $$h.setAttribute('autoComplete', 'off');
36 | $$h.addEventListener('focus', () => this.elem.classList.add('focus'));
37 | $$h.addEventListener('blur', () => {
38 | this.elem.classList.remove('focus');
39 | this.historyView.resetNavigation();
40 | });
41 | $$h.addEventListener('keydown', e => this.inputKeyDown(e));
42 | $$h.setAttribute('tabIndex', '0');
43 | return $$h;
44 | }.call(this));
45 | var $$i = document.createElement('div');
46 | $$i.setAttribute('class', 'actions');
47 | $$c.appendChild($$i);
48 | var $$j = document.createElement('a');
49 | $$j.setAttribute('href', 'login.php?action=logout');
50 | $$j.setAttribute('title', translation.logout);
51 | $$j.setAttribute('class', 'icon');
52 | $$i.appendChild($$j);
53 | var $$k = document.createTextNode('exit_to_app');
54 | $$j.appendChild($$k);
55 | $$b.appendChildren((this.loading = new Loading()).elem);
56 | $$a.appendChildren((this.historyView = new HistoryView()).elem);
57 | return $$a;
58 | }.call(this);
59 | }
60 | inputKeyDown(event) {
61 | switch (event.key || keyMapping[event.keyCode]) {
62 | case 'ArrowUp':
63 | this.historyView.navigateLater();
64 | event.preventDefault();
65 | break;
66 | case 'ArrowDown':
67 | this.historyView.navigateBefore();
68 | event.preventDefault();
69 | break;
70 | case 'Enter':
71 | statehandler.replace(this.historyView.index === -1 ? this.input.value : this.historyView.elements[this.historyView.index].query);
72 | event.preventDefault();
73 | break;
74 | case 'Escape':
75 | this.input.blur();
76 | this.historyView.resetNavigation();
77 | event.preventDefault();
78 | break;
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/res/js/component/nav.jsx:
--------------------------------------------------------------------------------
1 | const keyMapping = {
2 | 13: "Enter",
3 | 27: "Escape",
4 | 38: "ArrowUp",
5 | 40: "ArrowDown"
6 | };
7 |
8 | class Navigation extends Component {
9 | constructor() {
10 | super();
11 | this.render();
12 | }
13 |
14 | render() {
15 | return this.elem = (
16 |
17 |
18 |
19 |
20 |
search
21 | {this.input = (
22 |
this.elem.classList.add("focus")}
25 | onBlur={() => {
26 | this.elem.classList.remove("focus");
27 | this.historyView.resetNavigation();
28 | }}
29 | onKeyDown={(e) => this.inputKeyDown(e)}
30 | tabIndex="0"
31 | />
32 | )}
33 |
34 |
38 |
39 | {(this.loading = new Loading()).elem}
40 |
41 | {(this.historyView = new HistoryView()).elem}
42 |
43 | );
44 | }
45 |
46 | inputKeyDown(event) {
47 | switch (event.key || keyMapping[event.keyCode]) {
48 | case "ArrowUp":
49 | this.historyView.navigateLater();
50 | event.preventDefault();
51 | break;
52 | case "ArrowDown":
53 | this.historyView.navigateBefore();
54 | event.preventDefault();
55 | break;
56 | case "Enter":
57 | statehandler.replace(this.historyView.index === -1 ? this.input.value : this.historyView.elements[this.historyView.index].query);
58 | event.preventDefault();
59 | break;
60 | case "Escape":
61 | this.input.blur();
62 | this.historyView.resetNavigation();
63 | event.preventDefault();
64 | break;
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/res/js/component/nohistoryelement.js:
--------------------------------------------------------------------------------
1 | class NoHistoryElement {
2 | constructor() {
3 | this.render();
4 | }
5 | render() {
6 | return this.elem = function () {
7 | var $$a = document.createElement('p');
8 | $$a.appendChildren(translation.history.error_unavailable);
9 | return $$a;
10 | }.call(this);
11 | }
12 | }
--------------------------------------------------------------------------------
/res/js/component/nohistoryelement.jsx:
--------------------------------------------------------------------------------
1 | class NoHistoryElement {
2 | constructor() {
3 | this.render();
4 | }
5 |
6 | render() {
7 | return this.elem = (
8 | {translation.history.error_unavailable}
9 | );
10 | }
11 | }
--------------------------------------------------------------------------------
/res/js/util/component.js:
--------------------------------------------------------------------------------
1 | class Component {
2 | constructor() {
3 | this.eventListeners = {};
4 | }
5 |
6 | addEventListener(type, handler) {
7 | this.getListeners(type).push(handler);
8 | }
9 |
10 | removeEventListener(type, handler) {
11 | const listeners = this.getListeners(type);
12 | listeners.splice(listeners.indexOf(handler), 1);
13 | }
14 |
15 | getListeners(type) {
16 | if (!this.eventListeners[type])
17 | this.eventListeners[type] = [];
18 | return this.eventListeners[type];
19 | }
20 |
21 | sendEvent(type, argv) {
22 | this.getListeners(type).forEach((listener) => {
23 | listener.apply(null, argv);
24 | })
25 | }
26 |
27 | setVisible(value) {
28 | if (this.elem)
29 | this.elem.classList.toggle("hidden", !value);
30 | }
31 | }
--------------------------------------------------------------------------------
/res/js/util/highlighthandler.js:
--------------------------------------------------------------------------------
1 | class HighlightHandler {
2 | render(text) {
3 |
4 | }
5 | }
--------------------------------------------------------------------------------
/res/js/util/loader.js:
--------------------------------------------------------------------------------
1 | function load(url, data = null) {
2 | return new Promise((resolve, reject) => {
3 | const encodeData = (data) => {
4 | const result = [];
5 | for (key in data) {
6 | if (data.hasOwnProperty(key)) {
7 | result.push(encodeURIComponent(key) + "=" + encodeURIComponent(data[key]));
8 | }
9 | }
10 | return result.join("&");
11 | };
12 |
13 | const request = new XMLHttpRequest();
14 | const method = data === null ? "GET" : "POST";
15 | request.onreadystatechange = () => {
16 | if (request.readyState === 4) {
17 | if (request.status >= 200 && request.status < 300) {
18 | try {
19 | resolve(JSON.parse(request.responseText));
20 | } catch (e) {
21 | reject(e);
22 | }
23 | } else {
24 | reject(request.status + ": " + request.responseText);
25 | }
26 | }
27 | };
28 | request.open(method, url, true);
29 | request.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
30 | if (data !== null) {
31 | request.send(encodeData(data));
32 | } else {
33 | request.send();
34 | }
35 | });
36 | }
--------------------------------------------------------------------------------
/res/js/util/mirccolorhandler.js:
--------------------------------------------------------------------------------
1 | class MircColorHandler {
2 | static render(text) {
3 | const CODE_BOLD = '\x02';
4 | const CODE_COLOR = '\x03';
5 | const CODE_HEXCOLOR = '\x04';
6 | const CODE_ITALIC = '\x1D';
7 | const CODE_UNDERLINE = '\x1F';
8 | const CODE_STRIKETHROUGH = '\x1E';
9 | const CODE_MONOSPACE = '\x11';
10 | const CODE_SWAP = '\x16';
11 | const CODE_RESET = '\x0F';
12 |
13 | const urlRegex = /\b((?:(?:mailto:|(?:[+.-]?\w)+:\/\/)|www(?=\.\S+\.))(?:(?:[,.;@:]?[-\w]+)+\.?|\[[0-9a-f:.]+])(?::\d+)?(?:\/(?:[,.;:]*[\w~@/?&=+$()!%#*-])*)?)(?:>|[,.;:"]*\b|$)/gi;
14 | const readNumber = function (str, start, end) {
15 | if (start >= end || start >= str.length)
16 | return -1;
17 | else
18 | return parseInt(str.substr(start, end - start), 10);
19 | };
20 | const findEndOfNumber = function (str, start) {
21 | const validCharCodes = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
22 | let i;
23 | let tmp = str.substr(start, 2);
24 | for (i = 0; i < 2 && i < tmp.length; i++) {
25 | if (validCharCodes.indexOf(tmp.charAt(i)) === -1)
26 | break;
27 | }
28 | return i + start;
29 | };
30 | const readHexNumber = function (str, start, end) {
31 | if (start >= end || start >= str.length)
32 | return -1;
33 | else
34 | return parseInt(str.substr(start, end - start), 16);
35 | };
36 | const findEndOfHexNumber = function (str, start) {
37 | const validCharCodes = [
38 | '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
39 | 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F'
40 | ];
41 | let i;
42 | let tmp = str.substr(start, 6);
43 | for (i = 0; i < 6 && i < tmp.length; i++) {
44 | if (validCharCodes.indexOf(tmp.charAt(i)) === -1)
45 | break;
46 | }
47 | return i + start;
48 | };
49 | const fromState = function (state) {
50 | const elem = document.createElement((state.url) ? 'a' : 'span');
51 | if (state.bold)
52 | elem.classList.add('irc_bold');
53 | if (state.italic)
54 | elem.classList.add('irc_italic');
55 | if (state.underline)
56 | elem.classList.add('irc_underline');
57 | if (state.strikethrough)
58 | elem.classList.add('irc_strikethrough');
59 | if (state.monospace)
60 | elem.classList.add('irc_monospace');
61 | if (state.foreground !== null)
62 | elem.dataset['irc_foreground'] = state.foreground;
63 | if (state.background !== null)
64 | elem.dataset['irc_background'] = state.background;
65 | if (state.hexforeground !== null)
66 | elem.style.color = '#' + ("000000" + state.hexforeground.toString(16)).substr(-6);
67 | if (state.highlight)
68 | elem.classList.add('irc_highlight');
69 | return elem;
70 | };
71 | const unescape = function (str) {
72 | return str.replace(/</g, '<').replace(/>/g, '>');
73 | };
74 | const apply = function (lastTag, str, i, normalCount, nodes) {
75 | const s = unescape(str.substr(i - normalCount, normalCount));
76 | if (normalCount === 0)
77 | return;
78 | if (lastTag.tagName === 'A') {
79 | lastTag.target = '_blank';
80 | lastTag.rel = 'noopener'
81 | if (s.indexOf('://') !== -1)
82 | lastTag.href = s;
83 | else
84 | lastTag.href = "http://" + s;
85 | }
86 | lastTag.appendChild(document.createTextNode(s));
87 | nodes.push(lastTag);
88 | };
89 | const urlStart = [];
90 | const urlEnd = [];
91 | let m;
92 | do {
93 | if (m) {
94 | urlStart.push(m.index);
95 | urlEnd.push(m.index + m[0].length);
96 | }
97 | m = urlRegex.exec(text);
98 | } while (m);
99 | const formatString = function (str) {
100 | if (!str)
101 | return document.createTextNode('');
102 | let state = {
103 | bold: false,
104 | italic: false,
105 | underline: false,
106 | strikethrough: false,
107 | monospace: false,
108 | foreground: null,
109 | hexforeground: null,
110 | background: null,
111 | highlight: false,
112 | url: false
113 | };
114 | let lastTag = fromState(state);
115 | let nodes = [];
116 | let normalCount = 0;
117 | for (let i = 0; i < str.length; i++) {
118 | const character = str.charAt(i);
119 | if (urlEnd.includes(i)) {
120 | apply(lastTag, str, i, normalCount, nodes);
121 | normalCount = 0;
122 | state.url = false;
123 | lastTag = fromState(state);
124 | }
125 | if (urlStart.includes(i)) {
126 | apply(lastTag, str, i, normalCount, nodes);
127 | normalCount = 0;
128 | state.url = true;
129 | lastTag = fromState(state);
130 | }
131 | switch (character) {
132 | case CODE_BOLD: {
133 | apply(lastTag, str, i, normalCount, nodes);
134 | normalCount = 0;
135 | state.bold = !state.bold;
136 | lastTag = fromState(state);
137 | }
138 | break;
139 | case CODE_ITALIC: {
140 | apply(lastTag, str, i, normalCount, nodes);
141 | normalCount = 0;
142 | state.italic = !state.italic;
143 | lastTag = fromState(state);
144 | }
145 | break;
146 | case CODE_UNDERLINE: {
147 | apply(lastTag, str, i, normalCount, nodes);
148 | normalCount = 0;
149 | state.underline = !state.underline;
150 | lastTag = fromState(state);
151 | }
152 | break;
153 | case CODE_STRIKETHROUGH: {
154 | apply(lastTag, str, i, normalCount, nodes);
155 | normalCount = 0;
156 | state.strikethrough = !state.strikethrough;
157 | lastTag = fromState(state);
158 | }
159 | break;
160 | case CODE_MONOSPACE: {
161 | apply(lastTag, str, i, normalCount, nodes);
162 | normalCount = 0;
163 | state.monospace = !state.monospace;
164 | lastTag = fromState(state);
165 | }
166 | break;
167 | case CODE_COLOR: {
168 | apply(lastTag, str, i, normalCount, nodes);
169 | normalCount = 0;
170 | let foregroundStart = i + 1;
171 | let foregroundEnd = findEndOfNumber(str, foregroundStart);
172 | if (foregroundEnd > foregroundStart) {
173 | let foreground = readNumber(str, foregroundStart, foregroundEnd);
174 | let background = -1;
175 | let backgroundStart = foregroundEnd + 1;
176 | let backgroundEnd = -1;
177 | if (str.length > foregroundEnd && str.charAt(foregroundEnd) === ',') {
178 | backgroundEnd = findEndOfNumber(str, backgroundStart);
179 | background = readNumber(str, backgroundStart, backgroundEnd);
180 | }
181 | if (state.foreground !== null) {
182 | if (background === -1)
183 | background = state.background;
184 | }
185 | if (foreground === -1) {
186 | state.foreground = null;
187 | state.hexforeground = null;
188 | } else {
189 | state.foreground = foreground;
190 | state.hexforeground = null;
191 | }
192 | state.background = background === -1 ? null : background;
193 | lastTag = fromState(state);
194 | i = (backgroundEnd === -1 ? foregroundEnd : backgroundEnd) - 1;
195 | } else if (state.foreground !== null) {
196 | state.foreground = null;
197 | state.background = null;
198 | lastTag = fromState(state);
199 | }
200 | }
201 | break;
202 | case CODE_HEXCOLOR: {
203 | apply(lastTag, str, i, normalCount, nodes);
204 | normalCount = 0;
205 | let foregroundStart = i + 1;
206 | let foregroundEnd = findEndOfHexNumber(str, foregroundStart);
207 | if (foregroundEnd > foregroundStart) {
208 | let foreground = readHexNumber(str, foregroundStart, foregroundEnd);
209 | if (foreground === -1) {
210 | state.hexforeground = null;
211 | state.foreground = null;
212 | } else {
213 | state.hexforeground = foreground;
214 | state.foreground = null;
215 | }
216 | lastTag = fromState(state);
217 | i = foregroundEnd - 1;
218 | } else if (state.hexforeground !== null) {
219 | state.hexforeground = null;
220 | lastTag = fromState(state);
221 | }
222 | }
223 | break;
224 | case CODE_SWAP: {
225 | apply(lastTag, str, i, normalCount, nodes);
226 | normalCount = 0;
227 | if (state.foreground !== null) {
228 | state.foreground = state.background;
229 | state.background = state.foreground;
230 | lastTag = fromState(state);
231 | }
232 | }
233 | break;
234 | case CODE_RESET: {
235 | apply(lastTag, str, i, normalCount, nodes);
236 | normalCount = 0;
237 | state.bold = false;
238 | state.italic = false;
239 | state.underline = false;
240 | state.foreground = null;
241 | state.background = null;
242 | lastTag = fromState(state);
243 | }
244 | break;
245 | case '<': {
246 | const start_tag = '';
247 | const end_tag = '';
248 | if (str.substr(i, start_tag.length) === start_tag) {
249 | apply(lastTag, str, i, normalCount, nodes);
250 | normalCount = 0;
251 | state.highlight = true;
252 | lastTag = fromState(state);
253 |
254 | i += start_tag.length - 1;
255 | } else if (str.substr(i, end_tag.length) === end_tag) {
256 | apply(lastTag, str, i, normalCount, nodes);
257 | normalCount = 0;
258 | state.highlight = false;
259 | lastTag = fromState(state);
260 |
261 | i += end_tag.length - 1;
262 | } else {
263 | normalCount++;
264 | }
265 | }
266 | break;
267 | default: {
268 | normalCount++;
269 | }
270 | break;
271 | }
272 | }
273 | apply(lastTag, str, str.length, normalCount, nodes);
274 | return nodes;
275 | };
276 | return formatString(text);
277 | }
278 | }
279 |
--------------------------------------------------------------------------------
/res/js/util/nativejsx-prototypes.js:
--------------------------------------------------------------------------------
1 | /******/ (function(modules) { // webpackBootstrap
2 | /******/ // The module cache
3 | /******/ var installedModules = {};
4 | /******/
5 | /******/ // The require function
6 | /******/ function __webpack_require__(moduleId) {
7 | /******/
8 | /******/ // Check if module is in cache
9 | /******/ if(installedModules[moduleId])
10 | /******/ return installedModules[moduleId].exports;
11 | /******/
12 | /******/ // Create a new module (and put it into the cache)
13 | /******/ var module = installedModules[moduleId] = {
14 | /******/ i: moduleId,
15 | /******/ l: false,
16 | /******/ exports: {}
17 | /******/ };
18 | /******/
19 | /******/ // Execute the module function
20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
21 | /******/
22 | /******/ // Flag the module as loaded
23 | /******/ module.l = true;
24 | /******/
25 | /******/ // Return the exports of the module
26 | /******/ return module.exports;
27 | /******/ }
28 | /******/
29 | /******/
30 | /******/ // expose the modules object (__webpack_modules__)
31 | /******/ __webpack_require__.m = modules;
32 | /******/
33 | /******/ // expose the module cache
34 | /******/ __webpack_require__.c = installedModules;
35 | /******/
36 | /******/ // identity function for calling harmony imports with the correct context
37 | /******/ __webpack_require__.i = function(value) { return value; };
38 | /******/
39 | /******/ // define getter function for harmony exports
40 | /******/ __webpack_require__.d = function(exports, name, getter) {
41 | /******/ if(!__webpack_require__.o(exports, name)) {
42 | /******/ Object.defineProperty(exports, name, {
43 | /******/ configurable: false,
44 | /******/ enumerable: true,
45 | /******/ get: getter
46 | /******/ });
47 | /******/ }
48 | /******/ };
49 | /******/
50 | /******/ // getDefaultExport function for compatibility with non-harmony modules
51 | /******/ __webpack_require__.n = function(module) {
52 | /******/ var getter = module && module.__esModule ?
53 | /******/ function getDefault() { return module['default']; } :
54 | /******/ function getModuleExports() { return module; };
55 | /******/ __webpack_require__.d(getter, 'a', getter);
56 | /******/ return getter;
57 | /******/ };
58 | /******/
59 | /******/ // Object.prototype.hasOwnProperty.call
60 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
61 | /******/
62 | /******/ // __webpack_public_path__
63 | /******/ __webpack_require__.p = "";
64 | /******/
65 | /******/ // Load entry module and return exports
66 | /******/ return __webpack_require__(__webpack_require__.s = 6);
67 | /******/ })
68 | /************************************************************************/
69 | /******/ ([
70 | /* 0 */
71 | /***/ (function(module, exports, __webpack_require__) {
72 |
73 | (function () {
74 | if (typeof HTMLElement.prototype.appendChildren !== 'function') {
75 | HTMLElement.prototype.appendChildren = function (children) {
76 | return __webpack_require__(3)(this, children)
77 | }
78 | }
79 | })()
80 |
81 |
82 | /***/ }),
83 | /* 1 */
84 | /***/ (function(module, exports, __webpack_require__) {
85 |
86 | (function () {
87 | if (typeof HTMLElement.prototype.setAttributes !== 'function') {
88 | HTMLElement.prototype.setAttributes = function (attributes) {
89 | return __webpack_require__(4)(this, attributes)
90 | }
91 | }
92 | })()
93 |
94 |
95 | /***/ }),
96 | /* 2 */
97 | /***/ (function(module, exports, __webpack_require__) {
98 |
99 | (function () {
100 | if (typeof HTMLElement.prototype.setStyles !== 'function') {
101 | HTMLElement.prototype.setStyles = function (styles) {
102 | return __webpack_require__(5)(this, styles)
103 | }
104 | }
105 | })()
106 |
107 |
108 | /***/ }),
109 | /* 3 */
110 | /***/ (function(module, exports) {
111 |
112 | module.exports = function appendChildren (element, children) {
113 | children = Array.isArray(children) ? children : [children]
114 | children.forEach(function (child) {
115 | if (child instanceof HTMLElement) {
116 | element.appendChild(child)
117 | } else if (child || typeof child === 'string') {
118 | element.appendChild(document.createTextNode(child.toString()))
119 | }
120 | })
121 | }
122 |
123 |
124 | /***/ }),
125 | /* 4 */
126 | /***/ (function(module, exports) {
127 |
128 | module.exports = function setAttributes (element, attributes) {
129 | var isPlainObject = Object.prototype.toString.call(attributes) === '[object Object]' &&
130 | typeof attributes.constructor === 'function' &&
131 | Object.prototype.toString.call(attributes.constructor.prototype) === '[object Object]' &&
132 | attributes.constructor.prototype.hasOwnProperty('isPrototypeOf')
133 |
134 | if (isPlainObject) {
135 | for (var key in attributes) {
136 | element.setAttribute(key, attributes[key])
137 | }
138 | } else {
139 | throw new DOMException('Failed to execute \'setAttributes\' on \'Element\': ' + Object.prototype.toString.call(attributes) + ' is not a plain object.')
140 | }
141 | }
142 |
143 |
144 | /***/ }),
145 | /* 5 */
146 | /***/ (function(module, exports) {
147 |
148 | module.exports = function setStyles (element, styles) {
149 | for (var style in styles) element.style[style] = styles[style]
150 | }
151 |
152 |
153 | /***/ }),
154 | /* 6 */
155 | /***/ (function(module, exports, __webpack_require__) {
156 |
157 | __webpack_require__(1)
158 | __webpack_require__(2)
159 | __webpack_require__(0)
160 |
161 |
162 | /***/ })
163 | /******/ ]);
--------------------------------------------------------------------------------
/res/js/util/sendercolorhandler.js:
--------------------------------------------------------------------------------
1 | class SenderColorHandler {
2 | static reflect(crc, n) {
3 | let j = 1, crcout = 0;
4 | for (let i = (1 << (n - 1)); i > 0; i >>= 1) {
5 | if ((crc & i) > 0) {
6 | crcout |= j;
7 | }
8 | j <<= 1;
9 | }
10 | return crcout;
11 | }
12 |
13 | static qChecksum(str) {
14 | let crc = 0xffff;
15 | const crcHighBitMask = 0x8000;
16 |
17 | for (let i = 0; i < str.length; i++) {
18 | const b = str.codePointAt(i);
19 | const c = SenderColorHandler.reflect(b, 8);
20 | for (let j = 0x80; j > 0; j >>= 1) {
21 | let highBit = crc & crcHighBitMask;
22 | crc <<= 1;
23 | if ((c & j) > 0) {
24 | highBit ^= crcHighBitMask;
25 | }
26 | if (highBit > 0) {
27 | crc ^= 0x1021;
28 | }
29 | }
30 | }
31 |
32 | crc = SenderColorHandler.reflect(crc, 16);
33 | crc ^= 0xffff;
34 | crc &= 0xffff;
35 |
36 | return crc;
37 | }
38 |
39 | static senderIndex(str) {
40 | const nickToHash = str.replace(/_*$/, "").toLowerCase();
41 | return SenderColorHandler.qChecksum(nickToHash) & 0xF;
42 | }
43 |
44 | static nickToColor(str) {
45 | return SenderColorHandler.senderIndex(str).toString(16);
46 | }
47 | }
--------------------------------------------------------------------------------
/res/js/util/statehandler.js:
--------------------------------------------------------------------------------
1 | class StateHandler extends Component {
2 | constructor() {
3 | super();
4 | this.state = null;
5 | window.addEventListener("hashchange", () => {
6 | this.update()
7 | });
8 | }
9 |
10 | init() {
11 | this.update();
12 | }
13 |
14 | replace(value) {
15 | if (this.state.length > 0 || value === this.state)
16 | history.replaceState(null, "", "#" + encodeURIComponent(value));
17 | else
18 | history.pushState(null, "", "#" + encodeURIComponent(value));
19 | this.update();
20 | }
21 |
22 | push(value) {
23 | history.pushState(null, "", "#" + encodeURIComponent(value));
24 | this.update();
25 | }
26 |
27 | update() {
28 | const oldState = this.state;
29 | this.state = decodeURIComponent(window.location.hash.substr(1));
30 |
31 | if (this.state !== oldState && this.state.length > 0)
32 | this.sendEvent("update", [this.state]);
33 | }
34 |
35 | parse(overrides = {}) {
36 | const options = {};
37 |
38 | function splitWithLimit(str, sep, n) {
39 | const out = [];
40 | let lastIndex = 0;
41 | let index;
42 | while (n-- > 1 && (index = str.indexOf(sep, lastIndex)) >= 0) {
43 | out.push(str.slice(lastIndex, index));
44 | lastIndex = index + sep.length;
45 | }
46 | out.push(str.slice(lastIndex));
47 | return out;
48 | }
49 |
50 | let query = [];
51 | const words = this.state.match(/\S+"[^"]+"|\S+/g);
52 | words.forEach((word) => {
53 | const parts = splitWithLimit(word, ":", 2);
54 | if ([
55 | "sender",
56 | "buffer",
57 | "network",
58 | "before",
59 | "since"
60 | ].includes(parts[0])) {
61 | options[parts[0]] = parts[1];
62 | } else {
63 | query.push(word);
64 | }
65 | });
66 | return {
67 | ...options,
68 | ...overrides,
69 | query: query.join(" ")
70 | }
71 | }
72 |
73 | clear() {
74 | this.replace("");
75 | }
76 | }
--------------------------------------------------------------------------------
/res/js/util/storage.js:
--------------------------------------------------------------------------------
1 | class Storage {
2 | static get(key) {
3 | try {
4 | return JSON.parse(localStorage.getItem(key));
5 | } catch (e) {
6 | return null;
7 | }
8 | }
9 |
10 | static set(key, value) {
11 | localStorage.setItem(key, JSON.stringify(value));
12 | }
13 |
14 | static exists(key) {
15 | return !!Storage.get(key);
16 | }
17 | }
--------------------------------------------------------------------------------
/templates/login.phtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/templates/search.phtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/translations/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "buffer": {
3 | "open": "Öffnen",
4 | "close": "Schließen"
5 | },
6 | "results": {
7 | "show_more": "Mehr Suchergebnisse anzeigen",
8 | "load_more": "Mehr Suchergebnisse laden"
9 | },
10 | "context": {
11 | "load_later": "Ältere Nachrichten Laden",
12 | "load_earlier": "Neuere Nachrichten Laden",
13 | "show_hide": "Nachrichtenkontext anzeigen/verstecken"
14 | },
15 | "history": {
16 | "error_unavailable": "Keine vorherigen Suchbegriffe vorhanden"
17 | },
18 | "login": {
19 | "username": "Benutzername",
20 | "password": "Passwort",
21 | "submit": "Anmelden",
22 | "message": {
23 | "success_logout": "Sie haben sich erfolgreich abgemeldet.",
24 | "error_invalid": "Falsche Benutzername/Passwort Kombination",
25 | "error_unauthed": "Sie müssen angemeldet sein, um diese Seite zu nutzen."
26 | }
27 | },
28 | "error": {
29 | "none_found": "Keine Ergebnisse gefunden"
30 | },
31 | "search": "Suchen",
32 | "logout": "Abmelden",
33 | "title": "QuasselSearch"
34 | }
--------------------------------------------------------------------------------
/translations/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "buffer": {
3 | "open": "Open",
4 | "close": "Close"
5 | },
6 | "results": {
7 | "show_more": "Show More Results",
8 | "load_more": "Load More Results"
9 | },
10 | "context": {
11 | "load_later": "Load Later Context",
12 | "load_earlier": "Load Earlier Context",
13 | "show_hide": "Toggle message context"
14 | },
15 | "history": {
16 | "error_unavailable": "No search history available"
17 | },
18 | "login": {
19 | "username": "Username",
20 | "password": "Password",
21 | "submit": "Log In",
22 | "message": {
23 | "success_logout": "You have successfully logged out.",
24 | "error_invalid": "Invalid username/password combination.",
25 | "error_unauthed": "You need to be logged in to access this page."
26 | }
27 | },
28 | "error": {
29 | "none_found": "No results found"
30 | },
31 | "search": "Search",
32 | "logout": "Logout",
33 | "title": "QuasselSearch"
34 | }
--------------------------------------------------------------------------------