diff --git a/assets/javascripts/discourse/api-initializers/discourse-group-timezones.gjs b/assets/javascripts/discourse/api-initializers/discourse-group-timezones.gjs
new file mode 100644
index 00000000..e07a8320
--- /dev/null
+++ b/assets/javascripts/discourse/api-initializers/discourse-group-timezones.gjs
@@ -0,0 +1,35 @@
+import { apiInitializer } from "discourse/lib/api";
+import GroupTimezones from "../components/group-timezones";
+
+const GroupTimezonesShim =
+
+;
+
+export default apiInitializer((api) => {
+ api.decorateCookedElement((element, helper) => {
+ element.querySelectorAll(".group-timezones").forEach((el) => {
+ const post = helper.getModel();
+
+ if (!post) {
+ return;
+ }
+
+ const group = el.dataset.group;
+ if (!group) {
+ throw new Error(
+ "Group timezone element is missing 'data-group' attribute"
+ );
+ }
+
+ helper.renderGlimmer(el, GroupTimezonesShim, {
+ group,
+ members: (post.group_timezones || {})[group] || [],
+ size: el.dataset.size || "medium",
+ });
+ });
+ });
+});
diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezones.js b/assets/javascripts/discourse/components/group-timezones/index.gjs
similarity index 57%
rename from assets/javascripts/discourse/widgets/discourse-group-timezones.js
rename to assets/javascripts/discourse/components/group-timezones/index.gjs
index 49f7ae23..98d8e75b 100644
--- a/assets/javascripts/discourse/widgets/discourse-group-timezones.js
+++ b/assets/javascripts/discourse/components/group-timezones/index.gjs
@@ -1,38 +1,27 @@
-import hbs from "discourse/widgets/hbs-compiler";
-import { createWidget } from "discourse/widgets/widget";
-import roundTime from "../lib/round-time";
+import Component from "@glimmer/component";
+import { tracked } from "@glimmer/tracking";
+import { fn } from "@ember/helper";
+import { on } from "@ember/modifier";
+import { action } from "@ember/object";
+import { service } from "@ember/service";
+import { eq } from "truth-helpers";
+import { i18n } from "discourse-i18n";
+import roundTime from "../../lib/round-time";
+import NewDay from "./new-day";
+import TimeTraveller from "./time-traveller";
+import Timezone from "./timezone";
-export default createWidget("discourse-group-timezones", {
- tagName: "div.group-timezones",
+export default class GroupTimezones extends Component {
+ @service siteSettings;
- buildKey: (attrs) => `group-timezones-${attrs.id}`,
+ @tracked filter = "";
+ @tracked localTimeOffset = 0;
- buildClasses(attrs) {
- return attrs.size;
- },
-
- buildAttributes(attrs) {
- return {
- id: attrs.id,
- };
- },
-
- defaultState() {
- return {
- localTimeOffset: 0,
- };
- },
-
- onChangeCurrentUserTimeOffset(offset) {
- this.state.localTimeOffset = offset;
- },
-
- transform(attrs, state) {
- const members = attrs.members || [];
+ get groupedTimezones() {
let groupedTimezones = [];
- members.filterBy("timezone").forEach((member) => {
- if (this._shouldAddMemberToGroup(this.state.filter, member)) {
+ this.args.members.filterBy("timezone").forEach((member) => {
+ if (this.#shouldAddMemberToGroup(this.filter, member)) {
const timezone = member.timezone;
const identifier = parseInt(moment.tz(timezone).format("YYYYMDHm"), 10);
let groupedTimezone = groupedTimezones.findBy("identifier", identifier);
@@ -40,18 +29,18 @@ export default createWidget("discourse-group-timezones", {
if (groupedTimezone) {
groupedTimezone.members.push(member);
} else {
- const now = this._roundMoment(moment.tz(timezone));
- const workingDays = this._workingDays();
+ const now = this.#roundMoment(moment.tz(timezone));
+ const workingDays = this.#workingDays();
const offset = moment.tz(moment.utc(), timezone).utcOffset();
groupedTimezone = {
identifier,
offset,
type: "discourse-group-timezone",
- nowWithOffset: now.add(state.localTimeOffset, "minutes"),
- closeToWorkingHours: this._closeToWorkingHours(now, workingDays),
- inWorkingHours: this._inWorkingHours(now, workingDays),
- utcOffset: this._utcOffset(offset),
+ nowWithOffset: now.add(this.localTimeOffset, "minutes"),
+ closeToWorkingHours: this.#closeToWorkingHours(now, workingDays),
+ inWorkingHours: this.#inWorkingHours(now, workingDays),
+ utcOffset: this.#utcOffset(offset),
members: [member],
};
groupedTimezones.push(groupedTimezone);
@@ -84,36 +73,10 @@ export default createWidget("discourse-group-timezones", {
});
}
- return { groupedTimezones };
- },
+ return groupedTimezones;
+ }
- onChangeFilter(filter) {
- this.state.filter = filter && filter.length ? filter : null;
- },
-
- template: hbs`
- {{attach
- widget="discourse-group-timezones-header"
- attrs=(hash
- id=attrs.id
- group=attrs.group
- localTimeOffset=state.localTimeOffset
- )
- }}
-
- {{#each transformed.groupedTimezones as |groupedTimezone|}}
- {{attach
- widget=groupedTimezone.type
- attrs=(hash
- usersOnHoliday=attrs.usersOnHoliday
- groupedTimezone=groupedTimezone
- )
- }}
- {{/each}}
-
- `,
-
- _shouldAddMemberToGroup(filter, member) {
+ #shouldAddMemberToGroup(filter, member) {
if (filter) {
filter = filter.toLowerCase();
if (
@@ -127,17 +90,17 @@ export default createWidget("discourse-group-timezones", {
}
return false;
- },
+ }
- _roundMoment(date) {
- if (this.state.localTimeOffset) {
+ #roundMoment(date) {
+ if (this.localTimeOffset) {
date = roundTime(date);
}
return date;
- },
+ }
- _closeToWorkingHours(moment, workingDays) {
+ #closeToWorkingHours(moment, workingDays) {
const hours = moment.hours();
const startHour = this.siteSettings.working_day_start_hour;
const endHour = this.siteSettings.working_day_end_hour;
@@ -148,18 +111,18 @@ export default createWidget("discourse-group-timezones", {
(hours <= Math.min(endHour + extension, 23) && hours >= endHour)) &&
workingDays.includes(moment.isoWeekday())
);
- },
+ }
- _inWorkingHours(moment, workingDays) {
+ #inWorkingHours(moment, workingDays) {
const hours = moment.hours();
return (
hours > this.siteSettings.working_day_start_hour &&
hours < this.siteSettings.working_day_end_hour &&
workingDays.includes(moment.isoWeekday())
);
- },
+ }
- _utcOffset(offset) {
+ #utcOffset(offset) {
const sign = Math.sign(offset) === 1 ? "+" : "-";
offset = Math.abs(offset);
let hours = Math.floor(offset / 60).toString();
@@ -170,9 +133,9 @@ export default createWidget("discourse-group-timezones", {
/:00$/,
""
)}`.replace(/-0/, " ");
- },
+ }
- _workingDays() {
+ #workingDays() {
const enMoment = moment().locale("en");
const getIsoWeekday = (day) =>
enMoment.localeData()._weekdays.indexOf(day) || 7;
@@ -180,5 +143,40 @@ export default createWidget("discourse-group-timezones", {
.split("|")
.filter(Boolean)
.map((x) => getIsoWeekday(x));
- },
-});
+ }
+
+ @action
+ handleFilterChange(event) {
+ this.filter = event.target.value;
+ }
+
+
+
+
+ {{#each this.groupedTimezones key="identifier" as |groupedTimezone|}}
+ {{#if (eq groupedTimezone.type "discourse-group-timezone-new-day")}}
+
+ {{else}}
+
+ {{/if}}
+ {{/each}}
+
+
+}
diff --git a/assets/javascripts/discourse/components/group-timezones/new-day.gjs b/assets/javascripts/discourse/components/group-timezones/new-day.gjs
new file mode 100644
index 00000000..121c7cb9
--- /dev/null
+++ b/assets/javascripts/discourse/components/group-timezones/new-day.gjs
@@ -0,0 +1,16 @@
+import icon from "discourse/helpers/d-icon";
+
+const NewDay =
+
+
+ {{icon "chevron-left"}}
+ {{@beforeDate}}
+
+
+ {{@afterDate}}
+ {{icon "chevron-right"}}
+
+
+;
+
+export default NewDay;
diff --git a/assets/javascripts/discourse/components/group-timezones/time-traveller.gjs b/assets/javascripts/discourse/components/group-timezones/time-traveller.gjs
new file mode 100644
index 00000000..c6f07176
--- /dev/null
+++ b/assets/javascripts/discourse/components/group-timezones/time-traveller.gjs
@@ -0,0 +1,58 @@
+import Component from "@glimmer/component";
+import { on } from "@ember/modifier";
+import { action } from "@ember/object";
+import { not } from "truth-helpers";
+import DButton from "discourse/components/d-button";
+import roundTime from "../../lib/round-time";
+
+export default class TimeTraveller extends Component {
+ get localTimeWithOffset() {
+ let date = moment().add(this.args.localTimeOffset, "minutes");
+
+ if (this.args.localTimeOffset) {
+ date = roundTime(date);
+ }
+
+ return date.format("HH:mm");
+ }
+
+ @action
+ reset() {
+ this.args.setOffset(0);
+ }
+
+ @action
+ sliderMoved(event) {
+ const value = parseInt(event.target.value, 10);
+ const offset = value * 15;
+ this.args.setOffset(offset);
+ }
+
+
+
+
+ {{this.localTimeWithOffset}}
+
+
+
+
+
+
+
+
+
+
+
+}
diff --git a/assets/javascripts/discourse/components/group-timezones/timezone.gjs b/assets/javascripts/discourse/components/group-timezones/timezone.gjs
new file mode 100644
index 00000000..2e1f854a
--- /dev/null
+++ b/assets/javascripts/discourse/components/group-timezones/timezone.gjs
@@ -0,0 +1,44 @@
+import Component from "@glimmer/component";
+import UserAvatar from "discourse/components/user-avatar";
+import concatClass from "discourse/helpers/concat-class";
+
+export default class GroupTimezone extends Component {
+ get formattedTime() {
+ return this.args.groupedTimezone.nowWithOffset.format("LT");
+ }
+
+
+
+
+
+ {{this.formattedTime}}
+
+
+ {{@groupedTimezone.utcOffset}}
+
+
+
+ {{#each @groupedTimezone.members key="username" as |member|}}
+ -
+
+
+ {{/each}}
+
+
+
+}
diff --git a/assets/javascripts/discourse/initializers/discourse-group-timezones.js b/assets/javascripts/discourse/initializers/discourse-group-timezones.js
deleted file mode 100644
index 1d5f4d26..00000000
--- a/assets/javascripts/discourse/initializers/discourse-group-timezones.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import $ from "jquery";
-import { getRegister } from "discourse/lib/get-owner";
-import { withPluginApi } from "discourse/lib/plugin-api";
-import WidgetGlue from "discourse/widgets/glue";
-
-export default {
- name: "discourse-group-timezones",
-
- initialize() {
- withPluginApi("0.8.7", (api) => {
- let _glued = [];
-
- function cleanUp() {
- _glued.forEach((g) => g.cleanUp());
- _glued = [];
- }
-
- function _attachWidget(container, options) {
- const glue = new WidgetGlue(
- "discourse-group-timezones",
- getRegister(api),
- options
- );
- glue.appendTo(container);
- _glued.push(glue);
- }
-
- function _attachGroupTimezones($elem, post) {
- const $groupTimezones = $(".group-timezones", $elem);
-
- if (!$groupTimezones.length) {
- return;
- }
-
- $groupTimezones.each((idx, groupTimezone) => {
- const group = groupTimezone.getAttribute("data-group");
- if (!group) {
- throw "[group] attribute is necessary when using timezones.";
- }
-
- const members = (post.get("group_timezones") || {})[group] || [];
-
- _attachWidget(groupTimezone, {
- id: `${post.id}-${idx}`,
- members,
- group,
- usersOnHoliday:
- api.container.lookup("service:site").users_on_holiday || [],
- size: groupTimezone.getAttribute("data-size") || "medium",
- });
- });
- }
-
- function _attachPostWithGroupTimezones($elem, helper) {
- if (helper) {
- const post = helper.getModel();
-
- if (post) {
- api.preventCloak(post.id);
- _attachGroupTimezones($elem, post);
- }
- }
- }
-
- api.decorateCooked(_attachPostWithGroupTimezones, {
- id: "discourse-group-timezones",
- });
-
- api.cleanupStream(cleanUp);
- });
- },
-};
diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezone-new-day.js b/assets/javascripts/discourse/widgets/discourse-group-timezone-new-day.js
deleted file mode 100644
index fdf5e95f..00000000
--- a/assets/javascripts/discourse/widgets/discourse-group-timezone-new-day.js
+++ /dev/null
@@ -1,17 +0,0 @@
-import hbs from "discourse/widgets/hbs-compiler";
-import { createWidget } from "discourse/widgets/widget";
-
-export default createWidget("discourse-group-timezone-new-day", {
- tagName: "div.group-timezone-new-day",
-
- template: hbs`
-
- {{d-icon "chevron-left"}}
- {{this.attrs.groupedTimezone.beforeDate}}
-
-
- {{this.attrs.groupedTimezone.afterDate}}
- {{d-icon "chevron-right"}}
-
- `,
-});
diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezone.js b/assets/javascripts/discourse/widgets/discourse-group-timezone.js
deleted file mode 100644
index 6a856689..00000000
--- a/assets/javascripts/discourse/widgets/discourse-group-timezone.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import hbs from "discourse/widgets/hbs-compiler";
-import { createWidget } from "discourse/widgets/widget";
-
-export default createWidget("discourse-group-timezone", {
- tagName: "div.group-timezone",
-
- buildClasses(attrs) {
- const classes = [];
-
- if (attrs.groupedTimezone.closeToWorkingHours) {
- classes.push("close-to-working-hours");
- }
-
- if (attrs.groupedTimezone.inWorkingHours) {
- classes.push("in-working-hours");
- }
-
- return classes.join(" ");
- },
-
- transform(attrs) {
- return {
- formatedTime: attrs.groupedTimezone.nowWithOffset.format("LT"),
- };
- },
-
- template: hbs`
-
-
- {{transformed.formatedTime}}
-
-
- {{{attrs.groupedTimezone.utcOffset}}}
-
-
-
- {{#each attrs.groupedTimezone.members as |member|}}
- {{attach
- widget="discourse-group-timezones-member"
- attrs=(hash
- usersOnHoliday=attrs.usersOnHoliday
- member=member
- )
- }}
- {{/each}}
-
- `,
-});
diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezones-filter.js b/assets/javascripts/discourse/widgets/discourse-group-timezones-filter.js
deleted file mode 100644
index 3b38d55b..00000000
--- a/assets/javascripts/discourse/widgets/discourse-group-timezones-filter.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { throttle } from "@ember/runloop";
-import { createWidget } from "discourse/widgets/widget";
-import { i18n } from "discourse-i18n";
-
-export default createWidget("discourse-group-timezones-filter", {
- tagName: "input.group-timezones-filter",
-
- input(event) {
- this.changeFilterThrottler(event.target.value);
- },
-
- changeFilterThrottler(filter) {
- throttle(
- this,
- function () {
- this.sendWidgetAction("onChangeFilter", filter);
- },
- 100
- );
- },
-
- buildAttributes() {
- return {
- type: "text",
- placeholder: i18n("group_timezones.search"),
- };
- },
-});
diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezones-header.js b/assets/javascripts/discourse/widgets/discourse-group-timezones-header.js
deleted file mode 100644
index 867904f9..00000000
--- a/assets/javascripts/discourse/widgets/discourse-group-timezones-header.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import hbs from "discourse/widgets/hbs-compiler";
-import { createWidget } from "discourse/widgets/widget";
-import { i18n } from "discourse-i18n";
-
-export default createWidget("discourse-group-timezones-header", {
- tagName: "div.group-timezones-header",
-
- transform(attrs) {
- return {
- title: i18n("group_timezones.group_availability", {
- group: attrs.group,
- }),
- };
- },
-
- template: hbs`
- {{attach
- widget="discourse-group-timezones-time-traveler"
- attrs=(hash
- id=attrs.id
- localTimeOffset=attrs.localTimeOffset
- )
- }}
-
- {{transformed.title}}
-
- {{attach widget="discourse-group-timezones-filter"}}
- `,
-});
diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezones-member.js b/assets/javascripts/discourse/widgets/discourse-group-timezones-member.js
deleted file mode 100644
index e4f1a73e..00000000
--- a/assets/javascripts/discourse/widgets/discourse-group-timezones-member.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { h } from "virtual-dom";
-import { formatUsername } from "discourse/lib/utilities";
-import { avatarImg } from "discourse/widgets/post";
-import { createWidget } from "discourse/widgets/widget";
-
-export default createWidget("discourse-group-timezones-member", {
- tagName: "li.group-timezones-member",
-
- buildClasses(attrs) {
- return attrs.member.on_holiday ? "on-holiday" : "not-on-holiday";
- },
-
- html(attrs) {
- const { name, username, avatar_template } = attrs.member;
-
- return h(
- "a",
- {
- attributes: {
- class: "group-timezones-member-avatar",
- "data-user-card": username,
- },
- },
- avatarImg("small", {
- template: avatar_template,
- username: name || formatUsername(username),
- })
- );
- },
-});
diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezones-reset.js b/assets/javascripts/discourse/widgets/discourse-group-timezones-reset.js
deleted file mode 100644
index 99c23db2..00000000
--- a/assets/javascripts/discourse/widgets/discourse-group-timezones-reset.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import hbs from "discourse/widgets/hbs-compiler";
-import { createWidget } from "discourse/widgets/widget";
-
-export default createWidget("discourse-group-timezones-reset", {
- tagName: "div.group-timezones-reset",
-
- onResetOffset() {
- this.sendWidgetAction("onChangeCurrentUserTimeOffset", 0);
-
- const container = document.getElementById(this.attrs.id);
- const slider = container.querySelector(
- "input[type=range].group-timezones-slider"
- );
- if (slider) {
- slider.value = 0;
- }
- },
-
- transform(attrs) {
- return {
- isDisabled: attrs.localTimeOffset === 0,
- };
- },
-
- template: hbs`
- {{attach
- widget="button"
- attrs=(hash
- disabled=this.transformed.isDisabled
- action="onResetOffset"
- icon="arrow-rotate-left"
- )
- }}
- `,
-});
diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezones-slider.js b/assets/javascripts/discourse/widgets/discourse-group-timezones-slider.js
deleted file mode 100644
index 8b2ec430..00000000
--- a/assets/javascripts/discourse/widgets/discourse-group-timezones-slider.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import { throttle } from "@ember/runloop";
-import { createWidget } from "discourse/widgets/widget";
-
-export default createWidget("discourse-group-timezones-slider", {
- tagName: "input.group-timezones-slider",
-
- input(event) {
- this._handleSliderEvent(event);
- },
-
- change(event) {
- this._handleSliderEvent(event);
- },
-
- changeOffsetThrottler(offset) {
- throttle(
- this,
- function () {
- this.sendWidgetAction("onChangeCurrentUserTimeOffset", offset);
- },
- 75
- );
- },
-
- buildAttributes() {
- return {
- step: 1,
- value: 0,
- min: -48,
- max: 48,
- type: "range",
- };
- },
-
- _handleSliderEvent(event) {
- const value = parseInt(event.target.value, 10);
- const offset = value * 15;
- this.changeOffsetThrottler(offset);
- },
-});
diff --git a/assets/javascripts/discourse/widgets/discourse-group-timezones-time-traveler.js b/assets/javascripts/discourse/widgets/discourse-group-timezones-time-traveler.js
deleted file mode 100644
index b25cc4a4..00000000
--- a/assets/javascripts/discourse/widgets/discourse-group-timezones-time-traveler.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import hbs from "discourse/widgets/hbs-compiler";
-import { createWidget } from "discourse/widgets/widget";
-import roundTime from "../lib/round-time";
-
-export default createWidget("discourse-group-timezones-time-traveler", {
- tagName: "div.group-timezones-time-traveler",
-
- transform(attrs) {
- let date = moment().add(attrs.localTimeOffset, "minutes");
-
- if (attrs.localTimeOffset) {
- date = roundTime(date);
- }
-
- return {
- localTimeWithOffset: date.format("HH:mm"),
- };
- },
-
- template: hbs`
-
- {{transformed.localTimeWithOffset}}
-
-
- {{attach
- widget="discourse-group-timezones-slider"
- }}
-
- {{attach
- widget="discourse-group-timezones-reset"
- attrs=(hash
- id=attrs.id
- localTimeOffset=attrs.localTimeOffset
- )
- }}
- `,
-});
diff --git a/spec/system/group_timezones_spec.rb b/spec/system/group_timezones_spec.rb
new file mode 100644
index 00000000..b5fecbef
--- /dev/null
+++ b/spec/system/group_timezones_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+describe "Group timezones feature", type: :system do
+ fab!(:group) { Fabricate(:group, name: "test-group") }
+
+ fab!(:users) do
+ Fabricate
+ .times(5, :user)
+ .each do |user|
+ user.user_option.timezone = "America/New_York"
+ user.user_option.save!
+ group.add(user)
+ end
+ end
+
+ before do
+ Jobs.run_immediately!
+ SiteSetting.calendar_enabled = true
+ end
+
+ let(:post) { create_post(raw: <<~RAW) }
+ [timezones group="test-group"]
+ [/timezones]
+ RAW
+
+ it "renders successfully" do
+ visit(post.url)
+ expect(page).to have_selector(".group-timezones")
+ expect(page).to have_selector(".group-timezones-member", count: 5)
+ end
+end