diff --git a/client/css/style.css b/client/css/style.css index 03dfe793..f0487c55 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -80,6 +80,7 @@ button { code, kbd, +pre, .irc-monospace, textarea#user-specified-css-input { font-family: Consolas, Menlo, Monaco, "Lucida Console", "DejaVu Sans Mono", "Courier New", monospace; @@ -93,6 +94,19 @@ code { border-radius: 2px; } +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border-radius: 4px; +} + kbd { display: inline-block; font-size: 11px; @@ -140,12 +154,25 @@ kbd { opacity: 0.6; } +.btn-sm { + padding: 4px 8px; + border-width: 1px; + letter-spacing: 0; + word-spacing: 0; + text-transform: none; +} + .container { margin: 80px auto; max-width: 480px; touch-action: pan-y; } +#help .container, +#changelog .container { + max-width: 600px; +} + ::-moz-placeholder { color: rgba(0, 0, 0, 0.35); opacity: 1; @@ -208,7 +235,11 @@ kbd { #chat .nick .from::before, #chat .action .from::before, #chat .toggle-button::after, +.changelog-version::before, .context-menu-item::before, +#help .website-link::before, +#help .documentation-link::before, +#help .report-issue-link::before, #nick button::before, #image-viewer .previous-image-btn::before, #image-viewer .next-image-btn::before { @@ -259,6 +290,21 @@ kbd { color: #7f8c8d; } +#help .website-link::before, +#help .documentation-link::before, +#help .report-issue-link::before, +#chat .toggle-button { + display: inline-block; + margin-right: 5px; + /* These 2 directives are loosely taken from .fa-fw */ + width: 1.35em; + text-align: center; +} + +#help .website-link::before { content: "\f0ac"; /* http://fontawesome.io/icon/globe/ */ } +#help .documentation-link::before { content: "\f19d"; /* http://fontawesome.io/icon/graduation-cap/ */ } +#help .report-issue-link::before { content: "\f188"; /* http://fontawesome.io/icon/bug/ */ } + .session-list strong { display: block; } @@ -704,6 +750,7 @@ kbd { width: 100%; } +#windows li, #windows p, #windows label, #settings .error { @@ -761,6 +808,11 @@ kbd { padding-bottom: 7px; } +#windows .window h2 small { + color: inherit; + line-height: 30px; +} + #windows .window h3 { color: #7f8c8d; font-size: 18px; @@ -1520,8 +1572,7 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */ display: table-row; } -#help .help-item, -#help .about { +#help .help-item { font-size: 14px; } @@ -1540,10 +1591,6 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */ margin-bottom: 0; } -#help .about { - line-height: 1.8; -} - .is-apple #help .key-all, #help .key-apple { display: none; @@ -1572,6 +1619,66 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */ grid-column-start: 2; } +.changelog-text { + line-height: 1.5; +} + +.changelog-text p { + margin-bottom: 16px; +} + +.window#changelog h3 { + font-size: 20px; + border-bottom: 1px solid #7f8c8d; + color: #7f8c8d; + margin: 30px 0 10px; + padding-bottom: 7px; +} + +.changelog-version { + display: block; + padding: 16px; + margin-bottom: 16px; + border-radius: 2px; + background-color: #d9edf7; + color: #31708f; + transition: color 0.2s, background-color 0.2s; +} + +.changelog-version::before { + margin-right: 6px; + content: "\f250"; /* http://fontawesome.io/icon/hourglass-o/ */ +} + +.changelog-version.new-version { + color: #8a6d3b; + background-color: #fcf8e3; +} + +.changelog-version.new-version::before { + content: "\f087"; /* http://fontawesome.io/icon/thumbs-o-up/ */ +} + +.changelog-version.error { + color: #a94442; + background-color: #f2dede; +} + +.changelog-version.error::before { + margin-right: 6px; + content: "\f06a"; /* http://fontawesome.io/icon/exclamation-circle/ */ +} + +.changelog-version.up-to-date { + background-color: #dff0d8; + color: #3c763d; +} + +.changelog-version.up-to-date::before { + margin-right: 6px; + content: "\f00c"; /* http://fontawesome.io/icon/check/ */ +} + #form { background: #eee; border-top: 1px solid #ddd; @@ -2055,7 +2162,6 @@ part/quit messages where we don't load previews (adds a blank line otherwise) */ #windows .header .topic, #settings .error, #help .help-item, - #help .about, #loading, #context-menu, #form #input, diff --git a/client/index.html b/client/index.html index 1c3640e9..4c6a05b4 100644 --- a/client/index.html +++ b/client/index.html @@ -85,6 +85,7 @@
+
diff --git a/client/js/lounge.js b/client/js/lounge.js index 3e4c3158..d12ca562 100644 --- a/client/js/lounge.js +++ b/client/js/lounge.js @@ -332,65 +332,43 @@ $(function() { $(this).closest(".msg.condensed").toggleClass("closed"); }); - sidebar.on("click", ".chan, button", function(e, data) { - // Pushes states to history web API when clicking elements with a data-target attribute. - // States are very trivial and only contain a single `clickTarget` property which - // contains a CSS selector that targets elements which takes the user to a different view - // when clicked. The `popstate` event listener will trigger synthetic click events using that - // selector and thus take the user to a different view/state. - if (data && data.pushState === false) { - return; - } - const self = $(this); - const target = self.data("target"); - if (!target) { - return; - } - const state = {}; + let changelogRequestedAt = 0; - if (self.hasClass("chan")) { - state.clickTarget = `#sidebar .chan[data-id="${self.data("id")}"]`; - } else { - state.clickTarget = `#footer button[data-target="${target}"]`; - } - - if (history && history.pushState) { - if (data && data.replaceHistory && history.replaceState) { - history.replaceState(state, null, target); - } else { - history.pushState(state, null, target); - } - } - }); - - sidebar.on("click", ".chan, button", function() { + const openWindow = function openWindow(e, data) { var self = $(this); var target = self.data("target"); if (!target) { return; } - chat.data( - "id", - self.data("id") - ); - socket.emit( - "open", - self.data("id") - ); + // This is a rather gross hack to account for sources that are in the + // sidebar specifically. Needs to be done better when window management gets + // refactored. + const inSidebar = self.parents("#sidebar").length > 0; - sidebar.find(".active").removeClass("active"); - self.addClass("active") - .find(".badge") - .removeClass("highlight") - .empty(); + if (inSidebar) { + chat.data( + "id", + self.data("id") + ); + socket.emit( + "open", + self.data("id") + ); - if (sidebar.find(".highlight").length === 0) { - utils.toggleNotificationMarkers(false); + sidebar.find(".active").removeClass("active"); + self.addClass("active") + .find(".badge") + .removeClass("highlight") + .empty(); + + if (sidebar.find(".highlight").length === 0) { + utils.toggleNotificationMarkers(false); + } + + sidebarSlide.toggle(false); } - sidebarSlide.toggle(false); - var lastActive = $("#windows > .active"); lastActive @@ -447,8 +425,49 @@ $(function() { socket.emit("sessions:get"); } + if (target === "#help" || target === "#changelog") { + const now = Date.now(); + // Don't check more than once an hour + if (now - changelogRequestedAt > 3600 * 1000) { + changelogRequestedAt = now; + socket.emit("changelog"); + } + } + focus(); - }); + + // Pushes states to history web API when clicking elements with a data-target attribute. + // States are very trivial and only contain a single `clickTarget` property which + // contains a CSS selector that targets elements which takes the user to a different view + // when clicked. The `popstate` event listener will trigger synthetic click events using that + // selector and thus take the user to a different view/state. + if (data && data.pushState === false) { + return; + } + const state = {}; + + if (self.attr("id")) { + state.clickTarget = `#${self.attr("id")}`; + } else if (self.hasClass("chan")) { + state.clickTarget = `#sidebar .chan[data-id="${self.data("id")}"]`; + } else { + state.clickTarget = `#footer button[data-target="${target}"]`; + } + + if (history && history.pushState) { + if (data && data.replaceHistory && history.replaceState) { + history.replaceState(state, null, target); + } else { + history.pushState(state, null, target); + } + } + + return false; + }; + + sidebar.on("click", ".chan, button", openWindow); + $("#help").on("click", "#view-changelog, #back-to-help", openWindow); + $("#changelog").on("click", "#back-to-help", openWindow); sidebar.on("click", "#sign-out", function() { socket.emit("sign-out"); diff --git a/client/js/socket-events/changelog.js b/client/js/socket-events/changelog.js new file mode 100644 index 00000000..5eaf0229 --- /dev/null +++ b/client/js/socket-events/changelog.js @@ -0,0 +1,22 @@ +"use strict"; + +const $ = require("jquery"); +const socket = require("../socket"); +const templates = require("../../views"); + +socket.on("changelog", function(data) { + const container = $("#changelog-version-container"); + + if (data.latest) { + container.addClass("new-version"); + container.html(templates.new_version(data)); + } else if (data.current.changelog) { + container.addClass("up-to-date"); + container.text("The Lounge is up to date!"); + } else { + container.addClass("error"); + container.text("An error has occurred, try to reload the page."); + } + + $("#changelog").html(templates.windows.changelog(data)); +}); diff --git a/client/js/socket-events/configuration.js b/client/js/socket-events/configuration.js index fd687a7b..4d8f8309 100644 --- a/client/js/socket-events/configuration.js +++ b/client/js/socket-events/configuration.js @@ -14,6 +14,7 @@ socket.on("configuration", function(data) { $("#settings").html(templates.windows.settings(data)); $("#connect").html(templates.windows.connect(data)); $("#help").html(templates.windows.help(data)); + $("#changelog").html(templates.windows.changelog()); $("#play").on("click", () => { const pop = new Audio(); diff --git a/client/js/socket-events/index.js b/client/js/socket-events/index.js index f14c670a..0841459e 100644 --- a/client/js/socket-events/index.js +++ b/client/js/socket-events/index.js @@ -19,3 +19,4 @@ require("./users"); require("./sign_out"); require("./sessions_list"); require("./configuration"); +require("./changelog"); diff --git a/client/views/index.js b/client/views/index.js index 3ed35fba..4be9cc10 100644 --- a/client/views/index.js +++ b/client/views/index.js @@ -26,10 +26,12 @@ module.exports = { settings: require("./windows/settings.tpl"), connect: require("./windows/connect.tpl"), help: require("./windows/help.tpl"), + changelog: require("./windows/changelog.tpl"), }, chan: require("./chan.tpl"), chat: require("./chat.tpl"), + new_version: require("./new_version.tpl"), contextmenu_divider: require("./contextmenu_divider.tpl"), contextmenu_item: require("./contextmenu_item.tpl"), date_marker: require("./date-marker.tpl"), diff --git a/client/views/new_version.tpl b/client/views/new_version.tpl new file mode 100644 index 00000000..8825ed16 --- /dev/null +++ b/client/views/new_version.tpl @@ -0,0 +1,6 @@ +The Lounge {{latest.version}}{{#if latest.prerelease}} (pre-release){{/if}} +is now available. + + + Read more on GitHub + diff --git a/client/views/windows/changelog.tpl b/client/views/windows/changelog.tpl new file mode 100644 index 00000000..2ffd5c57 --- /dev/null +++ b/client/views/windows/changelog.tpl @@ -0,0 +1,20 @@ +
+ +
+
+ « Help + + {{#if current}} +

Release notes for {{current.version}}

+ + {{#if current.changelog}} +

Introduction

+
{{{current.changelog}}}
+ {{else}} +

Unable to retrieve releases from GitHub.

+

View release notes for this version on GitHub

+ {{/if}} + {{else}} +

Loading changelog…

+ {{/if}} +
diff --git a/client/views/windows/help.tpl b/client/views/windows/help.tpl index 35ef3ede..8f1e9b37 100644 --- a/client/views/windows/help.tpl +++ b/client/views/windows/help.tpl @@ -4,6 +4,51 @@

Help

+

+ + v{{version}} + (release notes) + + About The Lounge +

+ +
+ {{#unless public}} +

+ Checking for updates... +

+ {{/unless}} + + {{#if gitCommit}} +

+ The Lounge is running from source + (commit {{gitCommit}}). +

+ + + {{/if}} + +

+ Website +

+

+ Documentation +

+

+ Report an issue… +

+
+

Keyboard Shortcuts

@@ -11,7 +56,7 @@ Ctrl + /
-

Switch to the previous/next window in the channel list

+

Switch to the previous/next window in the channel list.

@@ -481,20 +526,4 @@

- -

About The Lounge

- -

- {{#if gitCommit}} - The Lounge is running from source - ({{ gitCommit }}).
- {{else}} - The Lounge is in version {{version}} - (See release notes).
- {{/if}} - - Website
- Documentation
- Report a bug -

diff --git a/src/plugins/changelog.js b/src/plugins/changelog.js new file mode 100644 index 00000000..9f604102 --- /dev/null +++ b/src/plugins/changelog.js @@ -0,0 +1,79 @@ +"use strict"; + +const pkg = require("../../package.json"); +const request = require("request"); + +module.exports = { + fetch, +}; + +const versions = { + current: { + version: `v${pkg.version}`, + }, +}; + +function fetch(callback) { + // Serving information from cache + if (versions.current.changelog) { + callback(versions); + return; + } + + request.get({ + uri: "https://api.github.com/repos/thelounge/lounge/releases", + headers: { + Accept: "application/vnd.github.v3.html", // Request rendered markdown + "User-Agent": pkg.name + "; +" + pkg.repository.git, // Identify the client + }, + }, (error, response, body) => { + if (error || response.statusCode !== 200) { + callback(versions); + return; + } + + let i; + let release; + let prerelease = false; + + body = JSON.parse(body); + + // Find the current release among releases on GitHub + for (i = 0; i < body.length; i++) { + release = body[i]; + if (release.tag_name === versions.current.version) { + versions.current.changelog = release.body_html; + prerelease = release.prerelease; + + break; + } + } + + // Find the latest release made after the current one if there is one + if (i > 0) { + for (let j = 0; j < i; j++) { + release = body[j]; + + // Find latest release or pre-release if current version is also a pre-release + if (!release.prerelease || release.prerelease === prerelease) { + versions.latest = { + prerelease: release.prerelease, + version: release.tag_name, + url: release.html_url, + }; + + break; + } + } + } + + // Emptying cached information after 15 minutes + setTimeout(() => { + delete versions.current.changelog; + delete versions.latest; + }, 15 * 60 * 1000 + ); + + callback(versions); + }); +} diff --git a/src/server.js b/src/server.js index 9d719d08..1522f77a 100644 --- a/src/server.js +++ b/src/server.js @@ -14,6 +14,7 @@ var colors = require("colors/safe"); const net = require("net"); const Identification = require("./identification"); const themes = require("./plugins/themes"); +const changelog = require("./plugins/changelog"); // The order defined the priority: the first available plugin is used // ALways keep local auth in the end, which should always be enabled. @@ -217,7 +218,7 @@ function index(req, res, next) { // If prefetch is enabled, but storage is not, we have to allow mixed content if (Helper.config.prefetchStorage || !Helper.config.prefetch) { - policies.push("img-src 'self'"); + policies.push("img-src 'self' https://user-images.githubusercontent.com"); policies.unshift("block-all-mixed-content"); } else { policies.push("img-src http: https:"); @@ -340,6 +341,12 @@ function initializeClient(socket, client, token, lastMessage) { } ); + socket.on("changelog", function() { + changelog.fetch((data) => { + socket.emit("changelog", data); + }); + }); + socket.on("msg:preview:toggle", function(data) { const networkAndChan = client.find(data.target); if (!networkAndChan) {