From db2c2b0b25e1ad16c6635641d638c23b16c5399f Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Thu, 3 Dec 2020 17:11:46 +0100 Subject: [PATCH] first round of styling for preview --- css/client.css | 48 +++++++++++++ css/create.css | 5 ++ css/main.css | 70 ++----------------- css/preview.css | 117 ++++++++++++++++++++++++++++++++ css/spinner.css | 27 ++++++++ images/chat-icon.svg | 4 ++ images/member-icon.svg | 7 ++ src/open/OpenLinkView.js | 16 ++--- src/preview/PreviewView.js | 37 +++++++--- src/preview/PreviewViewModel.js | 23 +++++-- 10 files changed, 267 insertions(+), 87 deletions(-) create mode 100644 css/client.css create mode 100644 css/create.css create mode 100644 css/preview.css create mode 100644 css/spinner.css create mode 100644 images/chat-icon.svg create mode 100644 images/member-icon.svg diff --git a/css/client.css b/css/client.css new file mode 100644 index 0000000..0ba3b9d --- /dev/null +++ b/css/client.css @@ -0,0 +1,48 @@ +.ClientListView .list { + padding: 16px 0; +} + +.ClientView { + border: 1px solid #E6E6E6; + border-radius: 8px; + margin: 16px 0; + padding: 16px; +} + +.ClientView .header { + display: flex; +} + +.ClientView .description { + flex: 1; +} + +.ClientView h3 { + margin-top: 0; +} + +.ClientView .icon { + border-radius: 8px; + background-repeat: no-repeat; + background-size: cover; + width: 60px; + height: 60px; +} + +.ClientView .icon.element-io { + background-image: url('../images/client-icons/element.svg'); +} + +.ClientView .icon.weechat { + background-image: url('../images/client-icons/weechat.svg'); +} + +.ClientView .actions a.badge { + display: inline-block; + height: 40px; + margin: 8px 16px 8px 0; +} + +.ClientView .actions img { + height: 100%; +} diff --git a/css/create.css b/css/create.css new file mode 100644 index 0000000..4739e14 --- /dev/null +++ b/css/create.css @@ -0,0 +1,5 @@ +.CreateLinkView h2 { + padding: 0 40px; + word-break: break-all; + text-align: center; +} diff --git a/css/main.css b/css/main.css index f78ad1d..7bcf364 100644 --- a/css/main.css +++ b/css/main.css @@ -14,6 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ +@import url('spinner.css'); +@import url('client.css'); +@import url('preview.css'); +@import url('create.css'); + :root { --app-background: #f4f4f4; --background: #ffffff; @@ -24,6 +29,8 @@ limitations under the License. --error: #d6001c; --link: #0098d4; --borders: #f4f4f4; + --lightgrey: #E6E6E6; + --spinner-stroke-size: 2px; } html { @@ -70,14 +77,6 @@ textarea { padding: 2rem; } -.PreviewView .preview { - text-align: center; -} - -.PreviewView .avatar { - border-radius: 100%; -} - .hidden { display: none !important; } @@ -134,45 +133,6 @@ button.text:hover { cursor: pointer; } -.ClientListView .list { - padding: 16px 0; -} - -.ClientView { - border: 1px solid #E6E6E6; - border-radius: 8px; - margin: 16px 0; - padding: 16px; -} - -.ClientView .header { - display: flex; -} - -.ClientView .description { - flex: 1; -} - -.ClientView h3 { - margin-top: 0; -} - -.ClientView .icon { - border-radius: 8px; - background-repeat: no-repeat; - background-size: cover; - width: 60px; - height: 60px; -} - -.ClientView .icon.element-io { - background-image: url('../images/client-icons/element.svg'); -} - -.ClientView .icon.weechat { - background-image: url('../images/client-icons/weechat.svg'); -} - .primary, .secondary { text-decoration: none; font-weight: bold; @@ -213,19 +173,3 @@ input[type='text'].large { width: 100%; box-sizing: border-box; } - -.ClientView .actions a.badge { - display: inline-block; - height: 40px; - margin: 8px 16px 8px 0; -} - -.ClientView .actions img { - height: 100%; -} - -.CreateLinkView h2 { - padding: 0 40px; - word-break: break-all; - text-align: center; -} \ No newline at end of file diff --git a/css/preview.css b/css/preview.css new file mode 100644 index 0000000..dced91c --- /dev/null +++ b/css/preview.css @@ -0,0 +1,117 @@ +.PreviewView { + text-align: center; + margin-bottom: 32px; +} + +.PreviewView h1 { + font-size: 24px; + line-height: 32px; + margin-bottom: 8px; +} + +.PreviewView .avatarContainer { + display: flex; + justify-content: center; + margin: 0; +} + +.PreviewView .avatar { + border-radius: 100%; + width: 64px; + height: 64px; +} + +.PreviewView .spinner { + width: 32px; + height: 32px; +} + +.PreviewView .avatar.loading { + border: 1px solid #eee; + display: flex; + align-items: center; + justify-content: center; +} + +.PreviewView .identifier { + color: var(--grey); + font-size: 12px; + margin: 8px 0; +} + +.PreviewView .identifier.placeholder { + height: 1em; + margin: 16px 30%; +} + +.PreviewView .memberCount { + display: flex; + justify-content: center; + margin: 8px 0; +} + +.PreviewView .memberCount p:not(.placeholder) { + background-image: url(../images/member-icon.svg); + background-repeat: no-repeat; + background-position: 2px center; + padding: 0 4px 0 24px; +} + +.PreviewView .memberCount.loading { + margin: 16px 0; +} + +.PreviewView .memberCount p { + background-color: var(--lightgrey); + padding: 0 4px; + border-radius: 8px; + font-size: 12px; + margin: 0; +} + +.PreviewView .memberCount p.placeholder { + height: 1.5em; + width: 80px; +} + +.PreviewView .topic { + font-size: 12px; + color: var(--grey); + margin: 32px; +} + +.PreviewView .topic.loading { + display: block; +} + +.PreviewView .topic.loading .placeholder { + height: 0.8em; + display: block; + margin: 12px 0; +} + +.PreviewView .topic.loading .placeholder:nth-child(2) { + margin-left: 5%; + margin-right: 5%; +} + +.placeholder { + border-radius: 1em; + --flash-bg: #ddd; + --flash-fg: #eee; + background: linear-gradient(120deg, + var(--flash-bg), + var(--flash-bg) 10%, + var(--flash-fg) calc(10% + 25px), + var(--flash-bg) calc(10% + 50px) + ); + animation: flash 2s ease-in-out infinite; + background-size: 200%; +} + +@keyframes flash { + 0% { background-position-x: 0; } + 50% { background-position-x: -80%; } + 51% { background-position-x: 40%; } + 100% { background-position-x: 0%; } +} diff --git a/css/spinner.css b/css/spinner.css new file mode 100644 index 0000000..4802dfc --- /dev/null +++ b/css/spinner.css @@ -0,0 +1,27 @@ +@keyframes rotate { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.spinner { + width: 40px; + height: 40px; + border-radius: 100%; + border: var(--spinner-stroke-size) solid var(--app-background); + box-sizing: border-box; +} + +.spinner::before { + content: ""; + display: block; + width: inherit; + height: inherit; + border-radius: 100%; + border-width: var(--spinner-stroke-size); + border-style: solid; + border-color: transparent; + border-top-color: var(--grey); + animation: rotate 0.8s linear infinite; + box-sizing: border-box; + margin: calc(-1 * var(--spinner-stroke-size)); +} diff --git a/images/chat-icon.svg b/images/chat-icon.svg new file mode 100644 index 0000000..c2b2913 --- /dev/null +++ b/images/chat-icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/images/member-icon.svg b/images/member-icon.svg new file mode 100644 index 0000000..f969c17 --- /dev/null +++ b/images/member-icon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/open/OpenLinkView.js b/src/open/OpenLinkView.js index 52efe87..c67e2ff 100644 --- a/src/open/OpenLinkView.js +++ b/src/open/OpenLinkView.js @@ -22,14 +22,12 @@ export class OpenLinkView extends TemplateView { render(t, vm) { return t.div({className: "OpenLinkView card"}, [ t.view(new PreviewView(vm.previewViewModel)), - t.div({className: {hidden: vm => vm.previewLoading}}, [ - t.p({className: {hidden: vm => vm.clientsViewModel}}, t.button({ - className: "primary fullwidth", - onClick: () => vm.showClients() - }, vm => vm.showClientsLabel)), - t.mapView(vm => vm.clientsViewModel, childVM => childVM ? new ClientListView(childVM) : null), - t.p(["Preview provided by ", vm => vm.previewDomain]), - ]) + t.p({className: {accept: true, hidden: vm => vm.clientsViewModel}}, t.button({ + className: "primary fullwidth", + onClick: () => vm.showClients() + }, vm => vm.showClientsLabel)), + t.mapView(vm => vm.clientsViewModel, childVM => childVM ? new ClientListView(childVM) : null), + t.p({className: {hidden: vm => !vm.previewDomain}}, ["Preview provided by ", vm => vm.previewDomain]), ]); } -} \ No newline at end of file +} diff --git a/src/preview/PreviewView.js b/src/preview/PreviewView.js index a80216c..825ab62 100644 --- a/src/preview/PreviewView.js +++ b/src/preview/PreviewView.js @@ -19,18 +19,35 @@ import {ClientListView} from "../open/ClientListView.js"; import {ClientView} from "../open/ClientView.js"; export class PreviewView extends TemplateView { + render(t, vm) { + return t.mapView(vm => vm.loading, loading => loading ? new LoadingPreviewView(vm) : new LoadedPreviewView(vm)); + } +} + +class LoadingPreviewView extends TemplateView { + render(t, vm) { + return t.div({className: "PreviewView"}, [ + t.div({className: "avatarContainer"}, t.div({className: "avatar loading"}, t.div({className: "spinner"}))), + t.h1(vm => vm.identifier), + t.p({className: "identifier placeholder"}), + t.div({className: {memberCount: true, loading: true, hidden: !vm.hasMemberCount}}, t.p({className: "placeholder"})), + t.p({className: {topic: true, loading: true, hidden: !vm.hasTopic}}, [ + t.div({className: "placeholder"}), + t.div({className: "placeholder"}), + t.div({className: "placeholder"}), + ]), + ]); + } +} + +class LoadedPreviewView extends TemplateView { render(t, vm) { return t.div({className: "PreviewView"}, [ - t.h1({className: {hidden: vm => !vm.loading}}, "Loading preview…"), - t.div({className: {hidden: vm => vm.loading}}, [ - t.div({className: "preview"}, [ - t.p(t.img({className: "avatar", src: vm => vm.avatarUrl})), - t.h1(vm => vm.name), - t.p({className: "identifier"}, vm => vm.identifier), - t.p({className: {memberCount: true, hidden: vm => !vm.memberCount}}, [vm => vm.memberCount, " members"]), - t.p({className: {topic: true, hidden: vm => !vm.topic}}, [vm => vm.topic]), - ]), - ]) + t.div({className: "avatarContainer"}, t.img({className: "avatar", src: vm => vm.avatarUrl})), + t.h1(vm => vm.name), + t.p({className: {identifier: true, hidden: vm => !vm.identifier}}, vm => vm.identifier), + t.div({className: {memberCount: true, hidden: vm => !vm.memberCount}}, t.p([vm => vm.memberCount, " members"])), + t.p({className: {topic: true, hidden: vm => !vm.topic}}, [vm => vm.topic]), ]); } } diff --git a/src/preview/PreviewViewModel.js b/src/preview/PreviewViewModel.js index 1d9f0f9..3f90daa 100644 --- a/src/preview/PreviewViewModel.js +++ b/src/preview/PreviewViewModel.js @@ -29,7 +29,7 @@ export class PreviewViewModel extends ViewModel { this.loading = false; this.name = null; this.avatarUrl = null; - this.identifier = null; + this.identifier = this._link.identifier; this.memberCount = null; this.topic = null; this.previewDomain = null; @@ -38,6 +38,7 @@ export class PreviewViewModel extends ViewModel { async load() { this.loading = true; this.emitChange(); + // await new Promise(r => setTimeout(r, 5000)); for (const server of this._consentedServers) { try { const homeserver = await resolveServer(this.request, server); @@ -51,15 +52,21 @@ export class PreviewViewModel extends ViewModel { } // assume we're done if nothing threw this.previewDomain = server; - break; + this.loading = false; + this.emitChange(); + return; } catch (err) { continue; } } - this.loading = false; - this.emitChange(); + this._setNoPreview(this._link); + this.loading = false; + this.emitChange(); } + get hasTopic() { return this._link.kind === LinkKind.Room; } + get hasMemberCount() { return this.hasTopic; } + async _loadUserPreview(homeserver, userId) { const profile = await homeserver.getUserProfile(userId); this.name = profile.displayname || userId; @@ -87,4 +94,10 @@ export class PreviewViewModel extends ViewModel { this.topic = publicRoom?.topic; this.identifier = publicRoom?.canonical_alias || link.identifier; } -} \ No newline at end of file + + _setNoPreview(link) { + this.name = link.identifier; + this.identifier = null; + this.avatarUrl = "images/chat-icon.svg"; + } +}