diff --git a/client/components/Chat.vue b/client/components/Chat.vue index f4b55a6a..998c626b 100644 --- a/client/components/Chat.vue +++ b/client/components/Chat.vue @@ -38,6 +38,11 @@ :network="network" :text="channel.topic" /> + + + + + + + + + + diff --git a/client/components/Windows/SearchResults.vue b/client/components/Windows/SearchResults.vue new file mode 100644 index 00000000..a833f9b0 --- /dev/null +++ b/client/components/Windows/SearchResults.vue @@ -0,0 +1,192 @@ + + + + + + + Search results for "{{ $route.params.term }}" in + {{ $route.params.target }} + + + + + + + + Loading… + Show older messages + + + + + Searching... + + + No results found. + + + + + + + + + + + + + + + diff --git a/client/css/style.css b/client/css/style.css index b825debb..e94991e4 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -285,6 +285,7 @@ p { #viewport .rt::before, #chat button.mentions::before, #chat button.menu::before, +#chat button.search::before, .channel-list-item::before, #footer .icon, #chat .count::before, @@ -339,6 +340,7 @@ p { #viewport .rt::before { content: "\f0c0"; /* https://fontawesome.com/icons/users?style=solid */ } #chat button.menu::before { content: "\f142"; /* http://fontawesome.io/icon/ellipsis-v/ */ } #chat button.mentions::before { content: "\f1fa"; /* https://fontawesome.com/icons/at?style=solid */ } +#chat button.search::before { content: "\f002"; /* https://fontawesome.com/icons/search?style=solid */ } .context-menu-join::before { content: "\f067"; /* http://fontawesome.io/icon/plus/ */ } .context-menu-user::before { content: "\f007"; /* http://fontawesome.io/icon/user/ */ } @@ -551,7 +553,9 @@ p { #viewport .lt, #viewport .rt, +<<<<<<< HEAD #chat button.mentions, +#chat button.search, #chat button.menu { color: #607992; display: flex; @@ -566,6 +570,7 @@ p { #viewport .lt::before, #viewport .rt::before, #chat button.mentions::before, +#chat button.search::before, #chat button.menu::before { width: 36px; line-height: 36px; /* Fix alignment in Microsoft Edge */ @@ -2817,3 +2822,72 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */ #chat table.channel-list .topic { white-space: pre-wrap; } + +form.message-search { + display: flex; +} + +form.message-search .input-wrapper { + display: flex; +} + +form.message-search button { + display: none !important; +} + +form.message-search input { + width: 100%; + height: auto !important; + margin: 7px 0; + border: 0; + color: inherit; + background-color: rgba(255, 255, 255, 0.1); +} + +form.message-search input::placeholder { + color: rgba(255, 255, 255, 0.35); +} + +@media (min-width: 480px) { + form.message-search input { + min-width: 140px; + transition: min-width 0.2s; + } + + form.message-search input:focus { + min-width: 220px; + } +} + +@media (max-width: 479px) { + form.message-search .input-wrapper { + position: absolute; + top: 45px; + left: 0; + right: 0; + z-index: 1; + height: 0; + transition: height 0.2s; + overflow: hidden; + background: var(--window-bg-color); + } + + form.message-search .input-wrapper input { + margin: 7px; + } + + form.message-search.opened .input-wrapper { + height: 50px; + } + + form.message-search button { + display: flex !important; + } +} + +.chat-view[data-type="search-results"] .search-status { + display: flex; + height: 100%; + justify-content: center; + align-items: center; +} diff --git a/client/js/router.js b/client/js/router.js index 1ead0f9f..05d6fbf4 100644 --- a/client/js/router.js +++ b/client/js/router.js @@ -13,6 +13,7 @@ import Settings from "../components/Windows/Settings.vue"; import Help from "../components/Windows/Help.vue"; import Changelog from "../components/Windows/Changelog.vue"; import NetworkEdit from "../components/Windows/NetworkEdit.vue"; +import SearchResults from "../components/Windows/SearchResults.vue"; import RoutedChat from "../components/RoutedChat.vue"; import store from "./store"; @@ -124,6 +125,11 @@ function initialize() { path: "/chan-:id", component: RoutedChat, }, + { + name: "SearchResults", + path: "/search/:uuid/:target/:term", + component: SearchResults, + }, ]); } diff --git a/client/js/socket-events/index.js b/client/js/socket-events/index.js index b0b62aa2..ce963d33 100644 --- a/client/js/socket-events/index.js +++ b/client/js/socket-events/index.js @@ -25,3 +25,4 @@ import "./changelog"; import "./setting"; import "./history_clear"; import "./mentions"; +import "./search"; diff --git a/client/js/socket-events/search.js b/client/js/socket-events/search.js new file mode 100644 index 00000000..f089f256 --- /dev/null +++ b/client/js/socket-events/search.js @@ -0,0 +1,7 @@ +import socket from "../socket"; +import store from "../store"; + +socket.on("search:results", (response) => { + store.commit("messageSearchInProgress", false); + store.commit("messageSearchResults", response); +}); diff --git a/client/js/store.js b/client/js/store.js index 3d336d06..a648779c 100644 --- a/client/js/store.js +++ b/client/js/store.js @@ -38,6 +38,8 @@ const store = new Vuex.Store({ versionStatus: "loading", versionDataExpired: false, serverHasSettings: false, + messageSearchResults: null, + messageSearchInProgress: false, }, mutations: { appLoaded(state) { @@ -112,12 +114,33 @@ const store = new Vuex.Store({ serverHasSettings(state, value) { state.serverHasSettings = value; }, + messageSearchInProgress(state, value) { + state.messageSearchInProgress = value; + }, + messageSearchResults(state, value) { + state.messageSearchResults = value; + }, }, getters: { findChannelOnCurrentNetwork: (state) => (name) => { name = name.toLowerCase(); return state.activeChannel.network.channels.find((c) => c.name.toLowerCase() === name); }, + findChannelOnNetwork: (state) => (networkUuid, channelName) => { + for (const network of state.networks) { + if (network.uuid !== networkUuid) { + continue; + } + + for (const channel of network.channels) { + if (channel.name === channelName) { + return {network, channel}; + } + } + } + + return null; + }, findChannel: (state) => (id) => { for (const network of state.networks) { for (const channel of network.channels) { diff --git a/src/client.js b/src/client.js index 5272863c..5d351920 100644 --- a/src/client.js +++ b/src/client.js @@ -528,6 +528,16 @@ Client.prototype.clearHistory = function (data) { } }; +Client.prototype.search = function(query) { + if (this.messageStorage) { + for (const storage of this.messageStorage) { + if (storage.database) { + return storage.search(query); + } + } + } +}; + Client.prototype.open = function (socketId, target) { // Due to how socket.io works internally, normal events may arrive later than // the disconnect event, and because we can't control this timing precisely, diff --git a/src/plugins/messageStorage/sqlite.js b/src/plugins/messageStorage/sqlite.js index 7fa0b6c2..16dc0ae4 100644 --- a/src/plugins/messageStorage/sqlite.js +++ b/src/plugins/messageStorage/sqlite.js @@ -202,9 +202,52 @@ class MessageStorage { }); } + search(query) { + if (!this.isEnabled) { + return Promise.resolve([]); + } + + let select = + 'SELECT msg, type, time, channel FROM messages WHERE type = "message" AND network = ? AND (json_extract(msg, "$.text") LIKE ? OR json_extract(msg, "$.from") LIKE ?)'; + const params = [query.networkUuid, `%${query.searchTerm}%`, `%${query.searchTerm}%`]; + + if (query.channelName) { + select += " AND channel = ? "; + params.push(query.channelName); + } + + select += " ORDER BY time DESC LIMIT 100 OFFSET ? "; + params.push(query.offset || 0); + + return new Promise((resolve, reject) => { + this.database.all(select, params, (err, rows) => { + if (err) { + reject(err); + } else { + const response = { + searchTerm: query.searchTerm, + target: query.channelName, + networkUuid: query.networkUuid, + offset: query.offset, + results: rows.map(parseRowToMessage), + }; + resolve(response); + } + }); + }); + } + canProvideMessages() { return this.isEnabled; } } module.exports = MessageStorage; + +function parseRowToMessage(row) { + const msg = JSON.parse(row.msg); + msg.time = row.time; + msg.type = row.type; + + return new Msg(msg); +} diff --git a/src/server.js b/src/server.js index f41554a2..454d621b 100644 --- a/src/server.js +++ b/src/server.js @@ -592,6 +592,12 @@ function initializeClient(socket, client, token, lastMessage, openChannel) { socket.on("sessions:get", sendSessionList); + socket.on("search", (query) => { + client.search(query).then((results) => { + socket.emit("search:results", results); + }); + }); + if (!Helper.config.public) { socket.on("setting:set", (newSetting) => { if (!newSetting || typeof newSetting !== "object") {