diff --git a/doc/SDK.md b/doc/SDK.md index d92b99c6..8ce0b304 100644 --- a/doc/SDK.md +++ b/doc/SDK.md @@ -33,7 +33,20 @@ import { RoomViewModel, TimelineView } from "hydrogen-view-sdk"; -import assetPaths from "hydrogen-view-sdk/paths/vite"; +import downloadSandboxPath from 'hydrogen-view-sdk/download-sandbox.html?url'; +import workerPath from 'hydrogen-view-sdk/main.js?url'; +import olmWasmPath from '@matrix-org/olm/olm.wasm?url'; +import olmJsPath from '@matrix-org/olm/olm.js?url'; +import olmLegacyJsPath from '@matrix-org/olm/olm_legacy.js?url'; +const assetPaths = { + downloadSandbox: downloadSandboxPath, + worker: workerPath, + olm: { + wasm: olmWasmPath, + legacyBundle: olmLegacyJsPath, + wasmBundle: olmJsPath + } +}; import "hydrogen-view-sdk/style.css"; async function main() { @@ -84,7 +97,13 @@ main(); ## Typescript support -There is partial typescript support while we are still in the process of converting the Hydrogen codebase to typesccript. +Typescript support is not yet available while we're converting the Hydrogen codebase to Typescript. +In your `src` directory, you'll need to add a `.d.ts` (can be called anything, e.g. `deps.d.ts`) +containing this snippet to make Typescript not complain that `hydrogen-view-sdk` doesn't have types: + +```ts +declare module "hydrogen-view-sdk"; +``` ## API Stability diff --git a/doc/TS-MIGRATION.md b/doc/TS-MIGRATION.md index d7ebc7f4..b63966f2 100644 --- a/doc/TS-MIGRATION.md +++ b/doc/TS-MIGRATION.md @@ -2,8 +2,37 @@ ## Use `type` rather than `interface` for named parameters and POJO return values. -`type` and `interface` can be used somewhat interchangebly used, but let's use `type` to describe data and `interface` to describe (polymorphic) behaviour. +`type` and `interface` can be used somewhat interchangeably, but let's use `type` to describe data and `interface` to describe (polymorphic) behaviour. Good examples of data are option objects to have named parameters, and POJO (plain old javascript objects) without any methods, just fields. Also see [this playground](https://www.typescriptlang.org/play?#code/C4TwDgpgBACghgJwgO2AeTMAlge2QZygF4oBvAKCiqmTgFsIAuKfYBLZAcwG5LqATCABs4IAPzNkAVzoAjCAl4BfcuVCQoAYQAWWIfwzY8hEvCSpDuAlABkZPlQDGOITgTNW7LstWOR+QjMUYHtqKGcCNilHYDcAChxMK3xmIIsk4wBKewcoFRVyPzgArV19KAgAD2AUfkDEYNDqCM9o2IQEjIJmHT0DLvxsijCw-ClIDsSjAkzeEebjEIYAuE5oEgADABJSKeSAOloGJSgsQh29433nVwQlDbnqfKA) + +## Use `type foo = { [key: string]: any }` for types that you intend to fill in later. + +For instance, if you have a method such as: +```js + function load(options) { + // ... + } +``` +and you intend to type options at some later point, do: +```ts + type Options = { [key: string]: any} +``` +This makes it much easier to add the necessary type information at a later time. + +## Use `object` or `Record` to describe a type that accepts any javascript object. + +Sometimes a function or method may genuinely need to accept any object; eg: +```js +function encodeBody(body) { + // ... +} +``` +In this scenario: +- Use `object` if you know that you will not access any property +- Use `Record` if you need to access some property + +Both usages prevent the type from accepting primitives (eg: string, boolean...). +If using `Record`, ensure that you have guards to check that the properties really do exist. diff --git a/package.json b/package.json index 1efafb16..8703a9df 100644 --- a/package.json +++ b/package.json @@ -45,13 +45,13 @@ "text-encoding": "^0.7.0", "typescript": "^4.3.5", "vite": "^2.6.14", - "xxhashjs": "^0.2.2" + "xxhashjs": "^0.2.2", + "bs58": "^4.0.1" }, "dependencies": { "@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz", "another-json": "^0.2.0", "base64-arraybuffer": "^0.2.0", - "bs58": "^4.0.1", "dompurify": "^2.3.0" } } diff --git a/scripts/sdk/base-manifest.json b/scripts/sdk/base-manifest.json index bc8919fb..d3e21d7b 100644 --- a/scripts/sdk/base-manifest.json +++ b/scripts/sdk/base-manifest.json @@ -1,15 +1,7 @@ { "name": "hydrogen-view-sdk", "description": "Embeddable matrix client library, including view components", - "version": "0.0.2", - "main": "./hydrogen.cjs.js", - "exports": { - ".": { - "import": "./hydrogen.es.js", - "require": "./hydrogen.cjs.js" - }, - "./paths/vite": "./paths/vite.js", - "./style.css": "./style.css" - }, - "types": "types/lib.d.ts" + "version": "0.0.4", + "main": "./hydrogen.es.js", + "type": "module" } diff --git a/scripts/sdk/build.sh b/scripts/sdk/build.sh index 3145df80..5534601e 100755 --- a/scripts/sdk/build.sh +++ b/scripts/sdk/build.sh @@ -1,10 +1,12 @@ +#!/bin/bash rm -rf target yarn run vite build -c vite.sdk-assets-config.js yarn run vite build -c vite.sdk-lib-config.js yarn tsc -p tsconfig-declaration.json ./scripts/sdk/create-manifest.js ./target/package.json mkdir target/paths -./scripts/sdk/transform-paths.js ./src/platform/web/sdk/paths/vite.js ./target/paths/vite.js +# this doesn't work, the ?url imports need to be in the consuming project, so disable for now +# ./scripts/sdk/transform-paths.js ./src/platform/web/sdk/paths/vite.js ./target/paths/vite.js cp doc/SDK.md target/README.md pushd target pushd asset-build/assets diff --git a/scripts/sdk/create-manifest.js b/scripts/sdk/create-manifest.js index 7a01de0a..b420e679 100755 --- a/scripts/sdk/create-manifest.js +++ b/scripts/sdk/create-manifest.js @@ -1,7 +1,29 @@ #!/usr/bin/env node const fs = require("fs"); -const appManifest = require("../../package.json") -const baseSDKManifest = require("./base-manifest.json") +const appManifest = require("../../package.json"); +const baseSDKManifest = require("./base-manifest.json"); +/* + need to leave exports out of base-manifest.json because of #vite-bug, + with the downside that we can't support environments that support + both esm and commonjs modules, so we pick just esm. + ``` + "exports": { + ".": { + "import": "./hydrogen.es.js", + "require": "./hydrogen.cjs.js" + }, + "./paths/vite": "./paths/vite.js", + "./style.css": "./style.css" + }, + ``` + + Also need to leave typescript type definitions out until the + typescript conversion is complete and all imports in the d.ts files + exists. + ``` + "types": "types/lib.d.ts" + ``` +*/ const mergeOptions = require('merge-options'); const manifestExtension = { diff --git a/src/domain/session/room/timeline/tiles/BaseMediaTile.js b/src/domain/session/room/timeline/tiles/BaseMediaTile.js index e5e62107..a927d766 100644 --- a/src/domain/session/room/timeline/tiles/BaseMediaTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMediaTile.js @@ -25,10 +25,8 @@ export class BaseMediaTile extends BaseMessageTile { super(options); this._decryptedThumbnail = null; this._decryptedFile = null; + this._isVisible = false; this._error = null; - if (!this.isPending) { - this._tryLoadEncryptedThumbnail(); - } } get isUploading() { @@ -60,6 +58,9 @@ export class BaseMediaTile extends BaseMessageTile { } get thumbnailUrl() { + if (!this._isVisible) { + return ""; + } if (this._decryptedThumbnail) { return this._decryptedThumbnail.url; } else { @@ -85,6 +86,15 @@ export class BaseMediaTile extends BaseMessageTile { return ""; } + notifyVisible() { + super.notifyVisible(); + this._isVisible = true; + this.emitChange("thumbnailUrl"); + if (!this.isPending) { + this._tryLoadEncryptedThumbnail(); + } + } + get width() { const info = this._getContent()?.info; return Math.round(info?.w * this._scaleFactor()); diff --git a/src/domain/session/room/timeline/tiles/LocationTile.js b/src/domain/session/room/timeline/tiles/LocationTile.js index fddc4501..e5f7e1a3 100644 --- a/src/domain/session/room/timeline/tiles/LocationTile.js +++ b/src/domain/session/room/timeline/tiles/LocationTile.js @@ -16,21 +16,43 @@ limitations under the License. import {BaseMessageTile} from "./BaseMessageTile.js"; -/* -map urls: -apple: https://developer.apple.com/library/archive/featuredarticles/iPhoneURLScheme_Reference/MapLinks/MapLinks.html -android: https://developers.google.com/maps/documentation/urls/guide -wp: maps:49.275267 -122.988617 -https://www.habaneroconsulting.com/stories/insights/2011/opening-native-map-apps-from-the-mobile-browser -*/ export class LocationTile extends BaseMessageTile { + get shape() { + return "location"; + } + get mapsLink() { - const geoUri = this._getContent().geo_uri; - const [lat, long] = geoUri.split(":")[1].split(","); - return `maps:${lat} ${long}`; + try { + const url = new URL(this._getContent().geo_uri); + if (url.protocol !== "geo:") { + return ""; + } + const [locationStr, ...namedParams] = url.pathname.split(";"); + const [latStr, longStr] = locationStr.split(","); + const lat = parseFloat(latStr); + const long = parseFloat(longStr); + let uncertainty; + for (const namedParam of namedParams) { + const [name, value] = namedParam.split("="); + if (name === "u") { + uncertainty = parseFloat(value); + } + } + if (this.platform.isIOS) { + return `http://maps.apple.com/?ll=${lat},${long}`; + } else { + let uri = `geo:${lat},${long}`; + if (uncertainty) { + uri = uri + `;u=${uncertainty}`; + } + return uri; + } + } catch { + return ""; + } } get label() { - return `${this.sender} sent their location, click to see it in maps.`; + return this.i18n`${this.displayName} sent their location`; } } diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index 123b2cfc..90fee243 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -374,6 +374,18 @@ only loads when the top comes into view*/ animation-timing-function: linear; } +.Timeline_locationLink { + padding: 0px 8px; + border-radius: 16px; + border: 1px solid #e9edf1; + background-color: #f3f8fd; + text-decoration: none; + display: inline-block; + line-height: 2rem; + vertical-align: top; + margin: 1px 4px; +} + .AnnouncementView { margin: 5px 0; padding: 5px 10%; diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts index 68602c48..8e4c91a2 100644 --- a/src/platform/web/ui/session/room/TimelineView.ts +++ b/src/platform/web/ui/session/room/TimelineView.ts @@ -39,7 +39,6 @@ export interface TimelineViewModel extends IObservableValue { export type TileView = GapView | AnnouncementView | TextMessageView | ImageView | VideoView | FileView | MissingAttachmentView | RedactedView; - function bottom(node: HTMLElement): number { return node.offsetTop + node.clientHeight; } diff --git a/src/platform/web/ui/session/room/common.ts b/src/platform/web/ui/session/room/common.ts index 1018939d..0928a998 100644 --- a/src/platform/web/ui/session/room/common.ts +++ b/src/platform/web/ui/session/room/common.ts @@ -18,6 +18,7 @@ import {TextMessageView} from "./timeline/TextMessageView.js"; import {ImageView} from "./timeline/ImageView.js"; import {VideoView} from "./timeline/VideoView.js"; import {FileView} from "./timeline/FileView.js"; +import {LocationView} from "./timeline/LocationView.js"; import {MissingAttachmentView} from "./timeline/MissingAttachmentView.js"; import {AnnouncementView} from "./timeline/AnnouncementView.js"; import {RedactedView} from "./timeline/RedactedView.js"; @@ -41,6 +42,8 @@ export function viewClassForEntry(entry: SimpleTile): TileViewConstructor | unde return VideoView; case "file": return FileView; + case "location": + return LocationView; case "missing-attachment": return MissingAttachmentView; case "redacted": diff --git a/src/platform/web/ui/session/room/timeline/ImageView.js b/src/platform/web/ui/session/room/timeline/ImageView.js index f4c1ecf7..1668b09c 100644 --- a/src/platform/web/ui/session/room/timeline/ImageView.js +++ b/src/platform/web/ui/session/room/timeline/ImageView.js @@ -19,7 +19,6 @@ import {BaseMediaView} from "./BaseMediaView.js"; export class ImageView extends BaseMediaView { renderMedia(t, vm) { const img = t.img({ - loading: "lazy", src: vm => vm.thumbnailUrl, alt: vm => vm.label, title: vm => vm.label, diff --git a/src/platform/web/ui/session/room/timeline/LocationView.js b/src/platform/web/ui/session/room/timeline/LocationView.js new file mode 100644 index 00000000..62073782 --- /dev/null +++ b/src/platform/web/ui/session/room/timeline/LocationView.js @@ -0,0 +1,27 @@ +/* +Copyright 2020 Bruno Windels + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {BaseMessageView} from "./BaseMessageView.js"; + +export class LocationView extends BaseMessageView { + renderMessageBody(t, vm) { + return t.p({className: "Timeline_messageBody statusMessage"}, [ + t.span(vm.label), + t.a({className: "Timeline_locationLink", href: vm.mapsLink, target: "_blank", rel: "noopener"}, vm.i18n`Click to open in maps`), + t.time(vm.date + " " + vm.time) + ]); + } +}