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";
+ }
+}