DEV: Convert various components to gjs (#26782)

Those were all low hanging fruits - all were already glimmer components, so this was mostly merging js and hbs files and adding imports.

(occasionally also adds/fixes class names)
This commit is contained in:
Jarek Radosz 2024-04-30 16:44:49 +02:00 committed by GitHub
parent 5d1f38a592
commit 3930064fd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 570 additions and 490 deletions

View File

@ -0,0 +1,19 @@
import Component from "@glimmer/component";
export default class PluginCommitHash extends Component {
get shortCommitHash() {
return this.args.plugin.commitHash?.slice(0, 7);
}
<template>
{{#if @plugin.commitHash}}
<a
href={{@plugin.commitUrl}}
target="_blank"
rel="noopener noreferrer"
class="current commit-hash"
title={{@plugin.commitHash}}
>{{this.shortCommitHash}}</a>
{{/if}}
</template>
}

View File

@ -1,9 +0,0 @@
{{#if this.commitHash}}
<a
href={{@plugin.commitUrl}}
target="_blank"
rel="noopener noreferrer"
class="current commit-hash"
title={{this.commitHash}}
>{{this.shortCommitHash}}</a>
{{/if}}

View File

@ -1,11 +0,0 @@
import Component from "@glimmer/component";
export default class PluginCommitHash extends Component {
get shortCommitHash() {
return this.commitHash?.slice(0, 7);
}
get commitHash() {
return this.args.plugin.commitHash;
}
}

View File

@ -0,0 +1,27 @@
import Component from "@glimmer/component";
import iconOrImage from "discourse/helpers/icon-or-image";
import domFromString from "discourse-common/lib/dom-from-string";
export default class BadgeButton extends Component {
get title() {
const description = this.args.badge?.description;
if (description) {
return domFromString(`<div>${description}</div>`)[0].innerText;
}
}
<template>
<span
title={{this.title}}
data-badge-name={{@badge.name}}
class="user-badge
{{@badge.badgeTypeClassName}}
{{unless @badge.enabled 'disabled'}}"
...attributes
>
{{iconOrImage @badge}}
<span class="badge-display-name">{{@badge.name}}</span>
{{yield}}
</span>
</template>
}

View File

@ -1,11 +0,0 @@
<span
class="user-badge
{{@badge.badgeTypeClassName}}
{{unless @badge.enabled 'disabled'}}"
title={{this.title}}
data-badge-name={{@badge.name}}
>
{{icon-or-image @badge}}
<span class="badge-display-name">{{@badge.name}}</span>
{{yield}}
</span>

View File

@ -1,12 +0,0 @@
import Component from "@glimmer/component";
import domFromString from "discourse-common/lib/dom-from-string";
// Takes @badge as argument.
export default class BadgeButtonComponent extends Component {
get title() {
const description = this.args.badge?.description;
if (description) {
return domFromString(`<div>${description}</div>`)[0].innerText;
}
}
}

View File

@ -1,6 +1,7 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { service } from "@ember/service";
import DButton from "discourse/components/d-button";
import DiscourseURL from "discourse/lib/url";
export default class BootstrapModeNotice extends Component {
@ -12,4 +13,12 @@ export default class BootstrapModeNotice extends Component {
`/t/-/${this.siteSettings.admin_quick_start_topic_id}`
);
}
<template>
<DButton
@action={{this.routeToAdminGuide}}
@label="bootstrap_mode"
class="btn-default bootstrap-mode"
/>
</template>
}

View File

@ -1,5 +0,0 @@
<DButton
class="btn-default bootstrap-mode"
@label="bootstrap_mode"
@action={{this.routeToAdminGuide}}
/>

View File

@ -0,0 +1,51 @@
import Component from "@glimmer/component";
import { inject as controller } from "@ember/controller";
import { action } from "@ember/object";
import { service } from "@ember/service";
import GroupCardContents from "discourse/components/group-card-contents";
import UserCardContents from "discourse/components/user-card-contents";
import routeAction from "discourse/helpers/route-action";
import DiscourseURL, { groupPath, userPath } from "discourse/lib/url";
import PluginOutlet from "./plugin-outlet";
export default class CardContainer extends Component {
@service site;
@controller topic;
@action
filterPosts(user) {
this.topic.send("filterParticipant", user);
}
@action
showUser(user) {
DiscourseURL.routeTo(userPath(user.username_lower));
}
@action
showGroup(group) {
DiscourseURL.routeTo(groupPath(group.name));
}
<template>
{{#if this.site.mobileView}}
<div class="card-cloak hidden"></div>
{{/if}}
<PluginOutlet @name="user-card-content-container">
<UserCardContents
@topic={{this.topic.model}}
@showUser={{this.showUser}}
@filterPosts={{this.filterPosts}}
@composePrivateMessage={{routeAction "composePrivateMessage"}}
role="dialog"
/>
</PluginOutlet>
<GroupCardContents
@topic={{this.topic.model}}
@showUser={{this.showUser}}
@showGroup={{this.showGroup}}
/>
</template>
}

View File

@ -1,19 +0,0 @@
{{#if this.site.mobileView}}
<div class="card-cloak hidden"></div>
{{/if}}
<PluginOutlet @name="user-card-content-container">
<UserCardContents
@topic={{this.topic.model}}
@showUser={{this.showUser}}
@filterPosts={{this.filterPosts}}
@composePrivateMessage={{route-action "composePrivateMessage"}}
role="dialog"
/>
</PluginOutlet>
<GroupCardContents
@topic={{this.topic.model}}
@showUser={{this.showUser}}
@showGroup={{this.showGroup}}
/>

View File

@ -1,26 +0,0 @@
import Component from "@glimmer/component";
import { inject as controller } from "@ember/controller";
import { action } from "@ember/object";
import { service } from "@ember/service";
import DiscourseURL, { groupPath, userPath } from "discourse/lib/url";
export default class CardWrapper extends Component {
@service site;
@controller topic;
@action
filterPosts(user) {
const topicController = this.topic;
topicController.send("filterParticipant", user);
}
@action
showUser(user) {
DiscourseURL.routeTo(userPath(user.username_lower));
}
@action
showGroup(group) {
DiscourseURL.routeTo(groupPath(group.name));
}
}

View File

@ -0,0 +1,23 @@
import Component from "@glimmer/component";
import DButton from "discourse/components/d-button";
import concatClass from "discourse/helpers/concat-class";
import { translateModKey } from "discourse/lib/utilities";
import I18n from "discourse-i18n";
export default class ComposerSaveButton extends Component {
get translatedTitle() {
return I18n.t("composer.title", { modifier: translateModKey("Meta+") });
}
<template>
<DButton
@action={{@action}}
@label={{@label}}
@icon={{@icon}}
@translatedTitle={{this.translatedTitle}}
@forwardEvent={{@forwardEvent}}
class={{concatClass "btn-primary create" (if @disabledSubmit "disabled")}}
...attributes
/>
</template>
}

View File

@ -1,9 +0,0 @@
<DButton
@translatedTitle={{this.translatedTitle}}
@label={{@label}}
@action={{@action}}
@icon={{@icon}}
@forwardEvent={{@forwardEvent}}
class="btn-primary create {{if @disabledSubmit 'disabled'}}"
...attributes
/>

View File

@ -1,9 +0,0 @@
import Component from "@glimmer/component";
import { translateModKey } from "discourse/lib/utilities";
import I18n from "discourse-i18n";
export default class ComposerSaveButton extends Component {
get translatedTitle() {
return I18n.t("composer.title", { modifier: translateModKey("Meta+") });
}
}

View File

@ -0,0 +1,57 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { htmlSafe } from "@ember/template";
import icon from "discourse-common/helpers/d-icon";
export default class FormTemplateFieldMultiSelect extends Component {
@action
isSelected(option) {
return this.args.value?.includes(option);
}
<template>
<div
data-field-type="multi-select"
class="control-group form-template-field"
>
{{#if @attributes.label}}
<label class="form-template-field__label">
{{@attributes.label}}
{{#if @validations.required}}
{{icon "asterisk" class="form-template-field__required-indicator"}}
{{/if}}
</label>
{{/if}}
{{#if @attributes.description}}
<span class="form-template-field__description">
{{htmlSafe @attributes.description}}
</span>
{{/if}}
{{! TODO(@keegan): Update implementation to use <MultiSelect/> instead }}
{{! Current using <select multiple> as it integrates easily with FormData (will update in v2) }}
<select
name={{@id}}
required={{if @validations.required "required" ""}}
multiple="multiple"
class="form-template-field__multi-select"
>
{{#if @attributes.none_label}}
<option
class="form-template-field__multi-select-placeholder"
value=""
disabled
hidden
>{{@attributes.none_label}}</option>
{{/if}}
{{#each @choices as |choice|}}
<option
value={{choice}}
selected={{this.isSelected choice}}
>{{choice}}</option>
{{/each}}
</select>
</div>
</template>
}

View File

@ -1,40 +0,0 @@
<div class="control-group form-template-field" data-field-type="multi-select">
{{#if @attributes.label}}
<label class="form-template-field__label">
{{@attributes.label}}
{{#if @validations.required}}
{{d-icon "asterisk" class="form-template-field__required-indicator"}}
{{/if}}
</label>
{{/if}}
{{#if @attributes.description}}
<span class="form-template-field__description">
{{html-safe @attributes.description}}
</span>
{{/if}}
{{! TODO(@keegan): Update implementation to use <MultiSelect/> instead }}
{{! Current using <select multiple> as it integrates easily with FormData (will update in v2) }}
<select
name={{@id}}
class="form-template-field__multi-select"
required={{if @validations.required "required" ""}}
multiple="multiple"
>
{{#if @attributes.none_label}}
<option
class="form-template-field__multi-select-placeholder"
value=""
disabled
hidden
>{{@attributes.none_label}}</option>
{{/if}}
{{#each @choices as |choice|}}
<option
value={{choice}}
selected={{this.isSelected choice}}
>{{choice}}</option>
{{/each}}
</select>
</div>

View File

@ -1,9 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
export default class FormTemplateFieldMultiSelect extends Component {
@action
isSelected(option) {
return this.args.value?.includes(option);
}
}

View File

@ -0,0 +1,24 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import bodyClass from "discourse/helpers/body-class";
import hideApplicationFooter from "discourse/helpers/hide-application-footer";
import loadingSpinner from "discourse/helpers/loading-spinner";
export default class LoadingSliderFallbackSpinner extends Component {
@service loadingSlider;
get shouldDisplay() {
const { mode, loading, stillLoading } = this.loadingSlider;
return (
(mode === "spinner" && loading) || (mode === "slider" && stillLoading)
);
}
<template>
{{#if this.shouldDisplay}}
<div class="route-loading-spinner">{{loadingSpinner}}</div>
{{bodyClass "has-route-loading-spinner"}}
{{hideApplicationFooter}}
{{/if}}
</template>
}

View File

@ -1,5 +0,0 @@
{{#if this.shouldDisplay}}
<div class="route-loading-spinner">{{loading-spinner}}</div>
{{body-class "has-route-loading-spinner"}}
{{hide-application-footer}}
{{/if}}

View File

@ -1,13 +0,0 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
export default class LoadingSliderFallbackSpinner extends Component {
@service loadingSlider;
get shouldDisplay() {
const { mode, loading, stillLoading } = this.loadingSlider;
return (
(mode === "spinner" && loading) || (mode === "slider" && stillLoading)
);
}
}

View File

@ -0,0 +1,31 @@
import Component from "@glimmer/component";
import { array } from "@ember/helper";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { service } from "@ember/service";
export default class ModalContainer extends Component {
@service modal;
@action
closeModal(data) {
this.modal.close(data);
}
<template>
<div
{{didInsert this.modal.setContainerElement}}
class="modal-container"
></div>
{{#if this.modal.activeModal}}
{{#each (array this.modal.activeModal) as |activeModal|}}
{{! #each ensures that the activeModal component/model are updated atomically }}
<activeModal.component
@model={{activeModal.opts.model}}
@closeModal={{this.closeModal}}
/>
{{/each}}
{{/if}}
</template>
}

View File

@ -1,12 +0,0 @@
<div class="modal-container" {{did-insert this.modal.setContainerElement}}>
</div>
{{#if this.modal.activeModal}}
{{#each (array this.modal.activeModal) as |activeModal|}}
{{! #each ensures that the activeModal component/model are updated atomically }}
<activeModal.component
@model={{activeModal.opts.model}}
@closeModal={{this.closeModal}}
/>
{{/each}}
{{/if}}

View File

@ -1,12 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { service } from "@ember/service";
export default class ModalContainer extends Component {
@service modal;
@action
closeModal(data) {
this.modal.close(data);
}
}

View File

@ -0,0 +1,38 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { fn } from "@ember/helper";
import DButton from "discourse/components/d-button";
import DModal from "discourse/components/d-modal";
import PreferenceCheckbox from "discourse/components/preference-checkbox";
import i18n from "discourse-common/helpers/i18n";
export default class DismissRead extends Component {
@tracked dismissTopics = false;
<template>
<DModal
@closeModal={{@closeModal}}
@title={{i18n @model.title count=@model.count}}
class="dismiss-read-modal"
>
<:body>
<p>
<PreferenceCheckbox
@labelKey="topics.bulk.also_dismiss_topics"
@checked={{this.dismissTopics}}
class="dismiss-read-modal__stop-tracking"
/>
</p>
</:body>
<:footer>
<DButton
@action={{fn @model.dismissRead this.dismissTopics}}
@label="topics.bulk.dismiss"
@icon="check"
id="dismiss-read-confirm"
class="btn-primary"
/>
</:footer>
</DModal>
</template>
}

View File

@ -1,24 +0,0 @@
<DModal
@closeModal={{@closeModal}}
@title={{i18n @model.title count=@model.count}}
class="dismiss-read-modal"
>
<:body>
<p>
<PreferenceCheckbox
@labelKey="topics.bulk.also_dismiss_topics"
@checked={{this.dismissTopics}}
class="dismiss-read-modal__stop-tracking"
/>
</p>
</:body>
<:footer>
<DButton
class="btn-primary"
@action={{fn @model.dismissRead this.dismissTopics}}
@icon="check"
id="dismiss-read-confirm"
@label="topics.bulk.dismiss"
/>
</:footer>
</DModal>

View File

@ -1,6 +0,0 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
export default class DismissRead extends Component {
@tracked dismissTopics = false;
}

View File

@ -1,6 +1,10 @@
import Component from "@glimmer/component";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import raw from "discourse/helpers/raw";
export default class NewListHeaderControlsWrapper extends Component {
@action
click(e) {
const target = e.target;
if (target.closest("button.topics-replies-toggle.--all")) {
@ -11,4 +15,20 @@ export default class NewListHeaderControlsWrapper extends Component {
this.args.changeNewListSubset("replies");
}
}
<template>
<div
{{! template-lint-disable no-invalid-interactive }}
{{on "click" this.click}}
class="topic-replies-toggle-wrapper"
>
{{raw
"list/new-list-header-controls"
current=@current
newRepliesCount=@newRepliesCount
newTopicsCount=@newTopicsCount
noStaticLabel=true
}}
</div>
</template>
}

View File

@ -1,13 +0,0 @@
<div
class="topic-replies-toggle-wrapper"
{{on "click" (action this.click)}}
{{! template-lint-disable no-invalid-interactive }}
>
{{raw
"list/new-list-header-controls"
current=@current
newRepliesCount=@newRepliesCount
newTopicsCount=@newTopicsCount
noStaticLabel=true
}}
</div>

View File

@ -0,0 +1,31 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { service } from "@ember/service";
import DButton from "discourse/components/d-button";
import i18n from "discourse-common/helpers/i18n";
export default class OfflineIndicator extends Component {
@service networkConnectivity;
get showing() {
return !this.networkConnectivity.connected;
}
@action
refresh() {
window.location.reload(true);
}
<template>
{{#if this.showing}}
<div class="offline-indicator">
<span>{{i18n "offline_indicator.no_internet"}}</span>
<DButton
@label="offline_indicator.refresh_page"
@display="link"
@action={{this.refresh}}
/>
</div>
{{/if}}
</template>
}

View File

@ -1,10 +0,0 @@
{{#if this.showing}}
<div class="offline-indicator">
<span>{{i18n "offline_indicator.no_internet"}}</span>
<DButton
@label="offline_indicator.refresh_page"
@display="link"
@action={{this.refresh}}
/>
</div>
{{/if}}

View File

@ -1,16 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { service } from "@ember/service";
export default class OfflineIndicator extends Component {
@service networkConnectivity;
get showing() {
return !this.networkConnectivity.connected;
}
@action
refresh() {
window.location.reload(true);
}
}

View File

@ -1,12 +1,15 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { cancel, next } from "@ember/runloop";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { eq } from "truth-helpers";
import concatClass from "discourse/helpers/concat-class";
import { bind } from "discourse-common/utils/decorators";
export default class extends Component {
export default class PageLoadingSlider extends Component {
@service loadingSlider;
@service capabilities;
@ -17,6 +20,11 @@ export default class extends Component {
this.loadingSlider.on("stateChanged", this.stateChanged);
}
willDestroy() {
super.willDestroy(...arguments);
this.loadingSlider.off("stateChange", this, "stateChange");
}
@bind
stateChanged(loading) {
if (this._deferredStateChange) {
@ -34,9 +42,9 @@ export default class extends Component {
}
}
destroy() {
this.loadingSlider.off("stateChange", this, "stateChange");
super.destroy();
get containerStyle() {
const duration = this.loadingSlider.averageLoadingDuration.toFixed(2);
return htmlSafe(`--loading-duration: ${duration}s`);
}
@action
@ -60,8 +68,23 @@ export default class extends Component {
}
}
get containerStyle() {
const duration = this.loadingSlider.averageLoadingDuration.toFixed(2);
return htmlSafe(`--loading-duration: ${duration}s`);
}
<template>
{{#if (eq this.loadingSlider.mode "slider")}}
<div
{{on "transitionend" this.onContainerTransitionEnd}}
style={{this.containerStyle}}
class={{concatClass
"loading-indicator-container"
this.state
(if this.capabilities.isAppWebview "discourse-hub-webview")
}}
>
<div
{{on "transitionend" this.onBarTransitionEnd}}
class="loading-indicator"
>
</div>
</div>
{{/if}}
</template>
}

View File

@ -1,17 +0,0 @@
{{#if (eq this.loadingSlider.mode "slider")}}
<div
class={{concat-class
"loading-indicator-container"
this.state
(if this.capabilities.isAppWebview "discourse-hub-webview")
}}
{{on "transitionend" this.onContainerTransitionEnd}}
style={{this.containerStyle}}
>
<div
class="loading-indicator"
{{on "transitionend" this.onBarTransitionEnd}}
>
</div>
</div>
{{/if}}

View File

@ -0,0 +1,13 @@
import Component from "@glimmer/component";
export default class MessagesSecondaryNav extends Component {
get messagesNav() {
return document.getElementById("user-navigation-secondary__horizontal-nav");
}
<template>
{{#in-element this.messagesNav}}
{{yield}}
{{/in-element}}
</template>
}

View File

@ -1,3 +0,0 @@
{{#in-element this.messagesNav}}
{{yield}}
{{/in-element}}

View File

@ -1,10 +0,0 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
export default class extends Component {
@service currentUser;
get messagesNav() {
return document.getElementById("user-navigation-secondary__horizontal-nav");
}
}

View File

@ -0,0 +1,51 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import emoji from "discourse/helpers/emoji";
import { until } from "discourse/lib/formatter";
import DTooltip from "float-kit/components/d-tooltip";
export default class UserStatusMessage extends Component {
@service currentUser;
get until() {
if (!this.args.status.ends_at) {
return;
}
const timezone = this.currentUser
? this.currentUser.user_option?.timezone
: moment.tz.guess();
return until(this.args.status.ends_at, timezone, this.currentUser?.locale);
}
<template>
{{#if @status}}
<DTooltip
@identifier="user-status-message-tooltip"
class="user-status-message"
...attributes
>
<:trigger>
{{emoji @status.emoji skipTitle=true}}
{{#if @showDescription}}
<span class="user-status-message-description">
{{@status.description}}
</span>
{{/if}}
</:trigger>
<:content>
{{emoji @status.emoji skipTitle=true}}
<span class="user-status-tooltip-description">
{{@status.description}}
</span>
{{#if this.until}}
<div class="user-status-tooltip-until">
{{this.until}}
</div>
{{/if}}
</:content>
</DTooltip>
{{/if}}
</template>
}

View File

@ -1,27 +0,0 @@
{{#if @status}}
<DTooltip
@identifier="user-status-message-tooltip"
class="user-status-message"
...attributes
>
<:trigger>
{{emoji @status.emoji skipTitle=true}}
{{#if @showDescription}}
<span class="user-status-message-description">
{{@status.description}}
</span>
{{/if}}
</:trigger>
<:content>
{{emoji @status.emoji skipTitle=true}}
<span class="user-status-tooltip-description">
{{@status.description}}
</span>
{{#if this.until}}
<div class="user-status-tooltip-until">
{{this.until}}
</div>
{{/if}}
</:content>
</DTooltip>
{{/if}}

View File

@ -1,19 +0,0 @@
import Component from "@glimmer/component";
import { service } from "@ember/service";
import { until } from "discourse/lib/formatter";
export default class UserStatusMessage extends Component {
@service currentUser;
get until() {
if (!this.args.status.ends_at) {
return;
}
const timezone = this.currentUser
? this.currentUser.user_option?.timezone
: moment.tz.guess();
return until(this.args.status.ends_at, timezone, this.currentUser?.locale);
}
}

View File

@ -1,4 +1,4 @@
import Component from "@ember/component";
import Component from "@glimmer/component";
import { service } from "@ember/service";
import { ajax } from "discourse/lib/ajax";
import isElementInViewport from "discourse/lib/is-element-in-viewport";
@ -8,8 +8,8 @@ import { bind } from "discourse-common/utils/decorators";
export default class WatchRead extends Component {
@service currentUser;
didInsertElement() {
super.didInsertElement(...arguments);
constructor() {
super(...arguments);
if (!this.currentUser || this.currentUser.read_faq) {
return;
@ -20,8 +20,8 @@ export default class WatchRead extends Component {
window.addEventListener("scroll", this._checkIfRead, false);
}
willDestroyElement() {
super.willDestroyElement(...arguments);
willDestroy() {
super.willDestroy(...arguments);
window.removeEventListener("resize", this._checkIfRead);
window.removeEventListener("scroll", this._checkIfRead);

View File

@ -0,0 +1,48 @@
import Component from "@glimmer/component";
function convertToSeconds(time) {
const match = time.toString().match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/);
const [hours, minutes, seconds] = match.slice(1);
if (hours || minutes || seconds) {
const h = parseInt(hours, 10) || 0;
const m = parseInt(minutes, 10) || 0;
const s = parseInt(seconds, 10) || 0;
return h * 3600 + m * 60 + s;
}
return time;
}
export default class LazyIframe extends Component {
get iframeSrc() {
switch (this.args.providerName) {
case "youtube":
let url = `https://www.youtube.com/embed/${this.args.videoId}?autoplay=1&rel=0`;
if (this.args.startTime) {
url += `&start=${convertToSeconds(this.args.startTime)}`;
}
return url;
case "vimeo":
return `https://player.vimeo.com/video/${this.args.videoId}${
this.args.videoId.includes("?") ? "&" : "?"
}autoplay=1`;
case "tiktok":
return `https://www.tiktok.com/embed/v2/${this.args.videoId}`;
}
}
<template>
{{#if @providerName}}
<iframe
src={{this.iframeSrc}}
title={{@title}}
allowFullScreen
scrolling="no"
frameborder="0"
seamless="seamless"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
></iframe>
{{/if}}
</template>
}

View File

@ -1,11 +0,0 @@
{{#if @providerName}}
<iframe
src={{this.iframeSrc}}
title={{@title}}
allowFullScreen
scrolling="no"
frameborder="0"
seamless="seamless"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
></iframe>
{{/if}}

View File

@ -1,34 +0,0 @@
import Component from "@glimmer/component";
export default class LazyVideo extends Component {
get iframeSrc() {
switch (this.args.providerName) {
case "youtube":
let url = `https://www.youtube.com/embed/${this.args.videoId}?autoplay=1&rel=0`;
if (this.args.startTime) {
url += `&start=${this.convertToSeconds(this.args.startTime)}`;
}
return url;
case "vimeo":
return `https://player.vimeo.com/video/${this.args.videoId}${
this.args.videoId.includes("?") ? "&" : "?"
}autoplay=1`;
case "tiktok":
return `https://www.tiktok.com/embed/v2/${this.args.videoId}`;
}
}
convertToSeconds(time) {
const match = time.toString().match(/(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/);
const [hours, minutes, seconds] = match.slice(1);
if (hours || minutes || seconds) {
const h = parseInt(hours, 10) || 0;
const m = parseInt(minutes, 10) || 0;
const s = parseInt(seconds, 10) || 0;
return h * 3600 + m * 60 + s;
}
return time;
}
}

View File

@ -0,0 +1,92 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { concat } from "@ember/helper";
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { htmlSafe } from "@ember/template";
import concatClass from "discourse/helpers/concat-class";
import LazyIframe from "./lazy-iframe";
export default class LazyVideo extends Component {
@tracked isLoaded = false;
get thumbnailStyle() {
const color = this.args.videoAttributes.dominantColor;
if (color?.match(/^[0-9A-Fa-f]+$/)) {
return htmlSafe(`background-color: #${color};`);
}
}
@action
loadEmbed() {
if (!this.isLoaded) {
this.isLoaded = true;
this.args.onLoadedVideo?.();
}
}
@action
onKeyPress(event) {
if (event.key === "Enter") {
event.preventDefault();
this.loadEmbed();
}
}
<template>
<div
data-video-id={{@videoAttributes.id}}
data-video-title={{@videoAttributes.title}}
data-video-start-time={{@videoAttributes.startTime}}
data-provider-name={{@videoAttributes.providerName}}
class={{concatClass
"lazy-video-container"
(concat @videoAttributes.providerName "-onebox")
(if this.isLoaded "video-loaded")
}}
>
{{#if this.isLoaded}}
<LazyIframe
@providerName={{@videoAttributes.providerName}}
@title={{@videoAttributes.title}}
@videoId={{@videoAttributes.id}}
@startTime={{@videoAttributes.startTime}}
/>
{{else}}
<div
{{on "click" this.loadEmbed}}
{{on "keypress" this.loadEmbed}}
tabindex="0"
style={{this.thumbnailStyle}}
class={{concatClass "video-thumbnail" @videoAttributes.providerName}}
>
<img
src={{@videoAttributes.thumbnail}}
title={{@videoAttributes.title}}
loading="lazy"
class={{concat @videoAttributes.providerName "-thumbnail"}}
/>
<div
class={{concatClass
"icon"
(concat @videoAttributes.providerName "-icon")
}}
></div>
</div>
<div class="title-container">
<div class="title-wrapper">
<a
href={{@videoAttributes.url}}
title={{@videoAttributes.title}}
target="_blank"
rel="noopener noreferrer"
class="title-link"
>
{{@videoAttributes.title}}
</a>
</div>
</div>
{{/if}}
</div>
</template>
}

View File

@ -1,54 +0,0 @@
<div
class={{concat-class
"lazy-video-container"
(concat @videoAttributes.providerName "-onebox")
(if this.isLoaded "video-loaded")
}}
data-video-id={{@videoAttributes.id}}
data-video-title={{@videoAttributes.title}}
data-video-start-time={{@videoAttributes.startTime}}
data-provider-name={{@videoAttributes.providerName}}
>
{{#if this.isLoaded}}
<LazyIframe
@providerName={{@videoAttributes.providerName}}
@title={{@videoAttributes.title}}
@videoId={{@videoAttributes.id}}
@startTime={{@videoAttributes.startTime}}
/>
{{else}}
<div
class={{concat-class "video-thumbnail" @videoAttributes.providerName}}
tabindex="0"
style={{this.thumbnailStyle}}
{{on "click" this.loadEmbed}}
{{on "keypress" this.loadEmbed}}
>
<img
class={{concat @videoAttributes.providerName "-thumbnail"}}
src={{@videoAttributes.thumbnail}}
title={{@videoAttributes.title}}
loading="lazy"
/>
<div
class={{concat-class
"icon"
(concat @videoAttributes.providerName "-icon")
}}
></div>
</div>
<div class="title-container">
<div class="title-wrapper">
<a
target="_blank"
rel="noopener noreferrer"
class="title-link"
href={{@videoAttributes.url}}
title={{@videoAttributes.title}}
>
{{@videoAttributes.title}}
</a>
</div>
</div>
{{/if}}
</div>

View File

@ -1,31 +0,0 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { htmlSafe } from "@ember/template";
export default class LazyVideo extends Component {
@tracked isLoaded = false;
@action
loadEmbed() {
if (!this.isLoaded) {
this.isLoaded = true;
this.args.onLoadedVideo?.();
}
}
@action
onKeyPress(event) {
if (event.key === "Enter") {
event.preventDefault();
this.loadEmbed();
}
}
get thumbnailStyle() {
const color = this.args.videoAttributes.dominantColor;
if (color?.match(/^[0-9A-Fa-f]+$/)) {
return htmlSafe(`background-color: #${color};`);
}
}
}