FEATURE: optional attached chat channel for event (#728)
This defines a feature where event creators can opt for an associated chat channel creation. This is a staff only feature for now. Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
This commit is contained in:
parent
5f1cf802dd
commit
8152a0ca7c
|
|
@ -3,18 +3,39 @@
|
||||||
module DiscoursePostEvent
|
module DiscoursePostEvent
|
||||||
class Event < ActiveRecord::Base
|
class Event < ActiveRecord::Base
|
||||||
PUBLIC_GROUP = "trust_level_0"
|
PUBLIC_GROUP = "trust_level_0"
|
||||||
|
MIN_NAME_LENGTH = 5
|
||||||
|
MAX_NAME_LENGTH = 255
|
||||||
self.table_name = "discourse_post_event_events"
|
self.table_name = "discourse_post_event_events"
|
||||||
|
|
||||||
self.ignored_columns = %w[starts_at ends_at]
|
self.ignored_columns = %w[starts_at ends_at]
|
||||||
|
|
||||||
has_many :event_dates, dependent: :destroy
|
has_many :event_dates, dependent: :destroy
|
||||||
|
# this is a cross plugin dependency, only called if chat is enabled
|
||||||
|
belongs_to :chat_channel, class_name: "Chat::Channel"
|
||||||
|
has_many :invitees, foreign_key: :post_id, dependent: :delete_all
|
||||||
|
belongs_to :post, foreign_key: :id
|
||||||
|
|
||||||
|
scope :visible, -> { where(deleted_at: nil) }
|
||||||
|
|
||||||
|
after_commit :destroy_topic_custom_field, on: %i[destroy]
|
||||||
|
after_commit :create_or_update_event_date, on: %i[create update]
|
||||||
|
before_save :chat_channel_sync
|
||||||
|
|
||||||
|
validate :raw_invitees_are_groups
|
||||||
|
validates :original_starts_at, presence: true
|
||||||
|
validates :name,
|
||||||
|
length: {
|
||||||
|
in: MIN_NAME_LENGTH..MAX_NAME_LENGTH,
|
||||||
|
},
|
||||||
|
unless: ->(event) { event.name.blank? }
|
||||||
|
|
||||||
|
validate :raw_invitees_length
|
||||||
|
validate :ends_before_start
|
||||||
|
validate :allowed_custom_fields
|
||||||
|
|
||||||
def self.attributes_protected_by_default
|
def self.attributes_protected_by_default
|
||||||
super - %w[id]
|
super - %w[id]
|
||||||
end
|
end
|
||||||
|
|
||||||
after_commit :destroy_topic_custom_field, on: %i[destroy]
|
|
||||||
def destroy_topic_custom_field
|
def destroy_topic_custom_field
|
||||||
if self.post && self.post.is_first_post?
|
if self.post && self.post.is_first_post?
|
||||||
TopicCustomField.where(
|
TopicCustomField.where(
|
||||||
|
|
@ -29,7 +50,6 @@ module DiscoursePostEvent
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
after_commit :create_or_update_event_date, on: %i[create update]
|
|
||||||
def create_or_update_event_date
|
def create_or_update_event_date
|
||||||
starts_at_changed = saved_change_to_original_starts_at
|
starts_at_changed = saved_change_to_original_starts_at
|
||||||
ends_at_changed = saved_change_to_original_ends_at
|
ends_at_changed = saved_change_to_original_ends_at
|
||||||
|
|
@ -84,11 +104,6 @@ module DiscoursePostEvent
|
||||||
ActiveSupport::Duration::PARTS.any? { |part| part.to_s == input }
|
ActiveSupport::Duration::PARTS.any? { |part| part.to_s == input }
|
||||||
end
|
end
|
||||||
|
|
||||||
has_many :invitees, foreign_key: :post_id, dependent: :delete_all
|
|
||||||
belongs_to :post, foreign_key: :id
|
|
||||||
|
|
||||||
scope :visible, -> { where(deleted_at: nil) }
|
|
||||||
|
|
||||||
def expired?
|
def expired?
|
||||||
(ends_at || starts_at.end_of_day) <= Time.now
|
(ends_at || starts_at.end_of_day) <= Time.now
|
||||||
end
|
end
|
||||||
|
|
@ -103,8 +118,6 @@ module DiscoursePostEvent
|
||||||
event_dates.order(:updated_at, :id).last&.ends_at
|
event_dates.order(:updated_at, :id).last&.ends_at
|
||||||
end
|
end
|
||||||
|
|
||||||
validates :original_starts_at, presence: true
|
|
||||||
|
|
||||||
def on_going_event_invitees
|
def on_going_event_invitees
|
||||||
return [] if !self.ends_at && self.starts_at < Time.now
|
return [] if !self.ends_at && self.starts_at < Time.now
|
||||||
|
|
||||||
|
|
@ -117,15 +130,6 @@ module DiscoursePostEvent
|
||||||
invitees.where(status: DiscoursePostEvent::Invitee.statuses[:going])
|
invitees.where(status: DiscoursePostEvent::Invitee.statuses[:going])
|
||||||
end
|
end
|
||||||
|
|
||||||
MIN_NAME_LENGTH = 5
|
|
||||||
MAX_NAME_LENGTH = 255
|
|
||||||
validates :name,
|
|
||||||
length: {
|
|
||||||
in: MIN_NAME_LENGTH..MAX_NAME_LENGTH,
|
|
||||||
},
|
|
||||||
unless: ->(event) { event.name.blank? }
|
|
||||||
|
|
||||||
validate :raw_invitees_length
|
|
||||||
def raw_invitees_length
|
def raw_invitees_length
|
||||||
if self.raw_invitees && self.raw_invitees.length > 10
|
if self.raw_invitees && self.raw_invitees.length > 10
|
||||||
errors.add(
|
errors.add(
|
||||||
|
|
@ -135,7 +139,6 @@ module DiscoursePostEvent
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
validate :raw_invitees_are_groups
|
|
||||||
def raw_invitees_are_groups
|
def raw_invitees_are_groups
|
||||||
if self.raw_invitees && User.select(:id).where(username: self.raw_invitees).limit(1).count > 0
|
if self.raw_invitees && User.select(:id).where(username: self.raw_invitees).limit(1).count > 0
|
||||||
errors.add(
|
errors.add(
|
||||||
|
|
@ -145,7 +148,6 @@ module DiscoursePostEvent
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
validate :ends_before_start
|
|
||||||
def ends_before_start
|
def ends_before_start
|
||||||
if self.original_starts_at && self.original_ends_at &&
|
if self.original_starts_at && self.original_ends_at &&
|
||||||
self.original_starts_at >= self.original_ends_at
|
self.original_starts_at >= self.original_ends_at
|
||||||
|
|
@ -156,7 +158,6 @@ module DiscoursePostEvent
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
validate :allowed_custom_fields
|
|
||||||
def allowed_custom_fields
|
def allowed_custom_fields
|
||||||
allowed_custom_fields = SiteSetting.discourse_post_event_allowed_custom_fields.split("|")
|
allowed_custom_fields = SiteSetting.discourse_post_event_allowed_custom_fields.split("|")
|
||||||
self.custom_fields.each do |key, value|
|
self.custom_fields.each do |key, value|
|
||||||
|
|
@ -174,8 +175,12 @@ module DiscoursePostEvent
|
||||||
attrs.map! do |attr|
|
attrs.map! do |attr|
|
||||||
{ post_id: self.id, created_at: timestamp, updated_at: timestamp }.merge(attr)
|
{ post_id: self.id, created_at: timestamp, updated_at: timestamp }.merge(attr)
|
||||||
end
|
end
|
||||||
|
result = self.invitees.insert_all!(attrs)
|
||||||
|
|
||||||
self.invitees.insert_all!(attrs)
|
# batch event does not call calleback
|
||||||
|
ChatChannelSync.sync(self) if chat_enabled?
|
||||||
|
|
||||||
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
def notify_invitees!(predefined_attendance: false)
|
def notify_invitees!(predefined_attendance: false)
|
||||||
|
|
@ -309,6 +314,7 @@ module DiscoursePostEvent
|
||||||
raw_invitees: event_params[:"allowed-groups"]&.split(","),
|
raw_invitees: event_params[:"allowed-groups"]&.split(","),
|
||||||
minimal: event_params[:minimal],
|
minimal: event_params[:minimal],
|
||||||
closed: event_params[:closed] || false,
|
closed: event_params[:closed] || false,
|
||||||
|
chat_enabled: event_params[:"chat-enabled"]&.downcase == "true",
|
||||||
}
|
}
|
||||||
|
|
||||||
params[:custom_fields] = {}
|
params[:custom_fields] = {}
|
||||||
|
|
@ -364,6 +370,15 @@ module DiscoursePostEvent
|
||||||
self.publish_update!
|
self.publish_update!
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def chat_channel_sync
|
||||||
|
if self.chat_enabled && self.chat_channel_id.blank? && post.last_editor_id.present?
|
||||||
|
DiscoursePostEvent::ChatChannelSync.sync(
|
||||||
|
self,
|
||||||
|
guardian: Guardian.new(User.find_by(id: post.last_editor_id)),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def calculate_next_date(start_date: nil)
|
def calculate_next_date(start_date: nil)
|
||||||
localized_start = start_date || original_starts_at.in_time_zone(timezone)
|
localized_start = start_date || original_starts_at.in_time_zone(timezone)
|
||||||
|
|
||||||
|
|
@ -408,4 +423,6 @@ end
|
||||||
# timezone :string
|
# timezone :string
|
||||||
# minimal :boolean
|
# minimal :boolean
|
||||||
# closed :boolean default(FALSE), not null
|
# closed :boolean default(FALSE), not null
|
||||||
|
# chat_enabled :boolean default(FALSE), not null
|
||||||
|
# chat_channel_id :bigint
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,10 @@ module DiscoursePostEvent
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
|
|
||||||
default_scope { joins(:user).includes(:user).where("users.id IS NOT NULL") }
|
default_scope { joins(:user).includes(:user).where("users.id IS NOT NULL") }
|
||||||
|
|
||||||
scope :with_status, ->(status) { where(status: Invitee.statuses[status]) }
|
scope :with_status, ->(status) { where(status: Invitee.statuses[status]) }
|
||||||
|
|
||||||
|
after_commit :sync_chat_channel_members
|
||||||
|
|
||||||
def self.statuses
|
def self.statuses
|
||||||
@statuses ||= Enum.new(going: 0, interested: 1, not_going: 2)
|
@statuses ||= Enum.new(going: 0, interested: 1, not_going: 2)
|
||||||
end
|
end
|
||||||
|
|
@ -45,6 +46,11 @@ module DiscoursePostEvent
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sync_chat_channel_members
|
||||||
|
return if !self.event.chat_enabled?
|
||||||
|
ChatChannelSync.sync(self.event)
|
||||||
|
end
|
||||||
|
|
||||||
def update_topic_tracking!
|
def update_topic_tracking!
|
||||||
topic_id = self.event.post.topic.id
|
topic_id = self.event.post.topic.id
|
||||||
user_id = self.user.id
|
user_id = self.user.id
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,16 @@ module DiscoursePostEvent
|
||||||
attributes :timezone
|
attributes :timezone
|
||||||
attributes :url
|
attributes :url
|
||||||
attributes :watching_invitee
|
attributes :watching_invitee
|
||||||
|
attributes :chat_enabled
|
||||||
|
attributes :channel
|
||||||
|
|
||||||
|
def channel
|
||||||
|
::Chat::ChannelSerializer.new(object.chat_channel, root: false, scope:)
|
||||||
|
end
|
||||||
|
|
||||||
|
def include_channel?
|
||||||
|
object.chat_enabled && defined?(::Chat::ChannelSerializer) && object.chat_channel.present?
|
||||||
|
end
|
||||||
|
|
||||||
def can_act_on_discourse_post_event
|
def can_act_on_discourse_post_event
|
||||||
scope.can_act_on_discourse_post_event?(object)
|
scope.can_act_on_discourse_post_event?(object)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
#
|
||||||
|
module DiscoursePostEvent
|
||||||
|
class ChatChannelSync
|
||||||
|
def self.sync(event, guardian: nil)
|
||||||
|
return if !event.chat_enabled?
|
||||||
|
if !event.chat_channel_id && guardian&.can_create_chat_channel?
|
||||||
|
ensure_chat_channel!(event, guardian:)
|
||||||
|
end
|
||||||
|
sync_chat_channel_members!(event) if event.chat_channel_id
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.sync_chat_channel_members!(event)
|
||||||
|
missing_members_sql = <<~SQL
|
||||||
|
SELECT user_id
|
||||||
|
FROM discourse_post_event_invitees
|
||||||
|
WHERE post_id = :post_id
|
||||||
|
AND status in (:statuses)
|
||||||
|
AND user_id NOT IN (
|
||||||
|
SELECT user_id
|
||||||
|
FROM user_chat_channel_memberships
|
||||||
|
WHERE chat_channel_id = :chat_channel_id
|
||||||
|
)
|
||||||
|
SQL
|
||||||
|
|
||||||
|
missing_user_ids =
|
||||||
|
DB.query_single(
|
||||||
|
missing_members_sql,
|
||||||
|
post_id: event.post.id,
|
||||||
|
statuses: [
|
||||||
|
DiscoursePostEvent::Invitee.statuses[:going],
|
||||||
|
DiscoursePostEvent::Invitee.statuses[:interested],
|
||||||
|
],
|
||||||
|
chat_channel_id: event.chat_channel_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if missing_user_ids.present?
|
||||||
|
ActiveRecord::Base.transaction do
|
||||||
|
missing_user_ids.each do |user_id|
|
||||||
|
event.chat_channel.user_chat_channel_memberships.create!(
|
||||||
|
user_id:,
|
||||||
|
chat_channel_id: event.chat_channel_id,
|
||||||
|
following: true,
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.ensure_chat_channel!(event, guardian:)
|
||||||
|
name = event.name
|
||||||
|
|
||||||
|
channel = nil
|
||||||
|
Chat::CreateCategoryChannel.call(
|
||||||
|
guardian:,
|
||||||
|
params: {
|
||||||
|
name:,
|
||||||
|
category_id: event.post.topic.category_id,
|
||||||
|
},
|
||||||
|
) do |result|
|
||||||
|
on_success { channel = result.channel }
|
||||||
|
on_failure { raise StandardError, result.inspect_steps }
|
||||||
|
end
|
||||||
|
|
||||||
|
# event creator will be a member of the channel
|
||||||
|
event.chat_channel_id = channel.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { LinkTo } from "@ember/routing";
|
||||||
|
import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
|
||||||
|
|
||||||
|
const DiscoursePostEventChatChannel = <template>
|
||||||
|
{{#if @event.channel}}
|
||||||
|
<section class="event__section event-chat-channel">
|
||||||
|
<span></span>
|
||||||
|
<LinkTo @route="chat.channel" @models={{@event.channel.routeModels}}>
|
||||||
|
<ChannelTitle @channel={{@event.channel}} />
|
||||||
|
</LinkTo>
|
||||||
|
</section>
|
||||||
|
{{/if}}
|
||||||
|
</template>;
|
||||||
|
|
||||||
|
export default DiscoursePostEventChatChannel;
|
||||||
|
|
@ -7,6 +7,7 @@ import concatClass from "discourse/helpers/concat-class";
|
||||||
import icon from "discourse/helpers/d-icon";
|
import icon from "discourse/helpers/d-icon";
|
||||||
import replaceEmoji from "discourse/helpers/replace-emoji";
|
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||||
import routeAction from "discourse/helpers/route-action";
|
import routeAction from "discourse/helpers/route-action";
|
||||||
|
import ChatChannel from "./chat-channel";
|
||||||
import Creator from "./creator";
|
import Creator from "./creator";
|
||||||
import Dates from "./dates";
|
import Dates from "./dates";
|
||||||
import EventStatus from "./event-status";
|
import EventStatus from "./event-status";
|
||||||
|
|
@ -124,10 +125,12 @@ export default class DiscoursePostEvent extends Component {
|
||||||
Dates=(component Dates event=@event)
|
Dates=(component Dates event=@event)
|
||||||
Invitees=(component Invitees event=@event)
|
Invitees=(component Invitees event=@event)
|
||||||
Status=(component Status event=@event)
|
Status=(component Status event=@event)
|
||||||
|
ChatChannel=(component ChatChannel event=@event)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Url @url={{@event.url}} />
|
<Url @url={{@event.url}} />
|
||||||
<Dates @event={{@event}} />
|
<Dates @event={{@event}} />
|
||||||
|
<ChatChannel @event={{@event}} />
|
||||||
<Invitees @event={{@event}} />
|
<Invitees @event={{@event}} />
|
||||||
{{#if @event.canUpdateAttendance}}
|
{{#if @event.canUpdateAttendance}}
|
||||||
<Status @event={{@event}} />
|
<Status @event={{@event}} />
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,14 @@
|
||||||
@outletArgs={{hash event=@model.event}}
|
@outletArgs={{hash event=@model.event}}
|
||||||
@connectorTagName="div"
|
@connectorTagName="div"
|
||||||
>
|
>
|
||||||
<DateTimeInputRange
|
<EventField>
|
||||||
@from={{this.startsAt}}
|
<DateTimeInputRange
|
||||||
@to={{this.endsAt}}
|
@from={{this.startsAt}}
|
||||||
@timezone={{@model.event.timezone}}
|
@to={{this.endsAt}}
|
||||||
@onChange={{this.onChangeDates}}
|
@timezone={{@model.event.timezone}}
|
||||||
/>
|
@onChange={{this.onChangeDates}}
|
||||||
|
/>
|
||||||
|
</EventField>
|
||||||
|
|
||||||
<EventField
|
<EventField
|
||||||
class="name"
|
class="name"
|
||||||
|
|
@ -223,6 +225,22 @@
|
||||||
</label>
|
</label>
|
||||||
</EventField>
|
</EventField>
|
||||||
|
|
||||||
|
{{#if this.showChat}}
|
||||||
|
<EventField
|
||||||
|
class="allow-chat"
|
||||||
|
@label="discourse_post_event.builder_modal.allow_chat.label"
|
||||||
|
>
|
||||||
|
<label class="checkbox-label">
|
||||||
|
<Input @type="checkbox" @checked={{@model.event.chatEnabled}} />
|
||||||
|
<span class="message">
|
||||||
|
{{i18n
|
||||||
|
"discourse_post_event.builder_modal.allow_chat.checkbox_label"
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</EventField>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if this.allowedCustomFields.length}}
|
{{#if this.allowedCustomFields.length}}
|
||||||
<EventField
|
<EventField
|
||||||
@label="discourse_post_event.builder_modal.custom_fields.label"
|
@label="discourse_post_event.builder_modal.custom_fields.label"
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export default class PostEventBuilder extends Component {
|
||||||
@service dialog;
|
@service dialog;
|
||||||
@service siteSettings;
|
@service siteSettings;
|
||||||
@service store;
|
@service store;
|
||||||
|
@service currentUser;
|
||||||
|
|
||||||
@tracked flash = null;
|
@tracked flash = null;
|
||||||
@tracked isSaving = false;
|
@tracked isSaving = false;
|
||||||
|
|
@ -131,6 +132,14 @@ export default class PostEventBuilder extends Component {
|
||||||
return this.event.reminders?.length >= 5;
|
return this.event.reminders?.length >= 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get showChat() {
|
||||||
|
// As of June 2025, chat channel creation is only available to admins and moderators
|
||||||
|
return (
|
||||||
|
this.siteSettings.chat_enabled &&
|
||||||
|
(this.currentUser.admin || this.currentUser.moderator)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
groupFinder(term) {
|
groupFinder(term) {
|
||||||
return Group.findAll({ term, ignore_automatic: true });
|
return Group.findAll({ term, ignore_automatic: true });
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ export function buildParams(startsAt, endsAt, event, siteSettings) {
|
||||||
params.minimal = "true";
|
params.minimal = "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (event.chatEnabled) {
|
||||||
|
params.chatEnabled = "true";
|
||||||
|
}
|
||||||
|
|
||||||
if (endsAt) {
|
if (endsAt) {
|
||||||
params.end = moment(endsAt).tz(eventTz).format("YYYY-MM-DD HH:mm");
|
params.end = moment(endsAt).tz(eventTz).format("YYYY-MM-DD HH:mm");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import EmberObject from "@ember/object";
|
||||||
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
import { TrackedArray } from "@ember-compat/tracked-built-ins";
|
||||||
import { bind } from "discourse/lib/decorators";
|
import { bind } from "discourse/lib/decorators";
|
||||||
import User from "discourse/models/user";
|
import User from "discourse/models/user";
|
||||||
|
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||||
import DiscoursePostEventEventStats from "./discourse-post-event-event-stats";
|
import DiscoursePostEventEventStats from "./discourse-post-event-event-stats";
|
||||||
import DiscoursePostEventInvitee from "./discourse-post-event-invitee";
|
import DiscoursePostEventInvitee from "./discourse-post-event-invitee";
|
||||||
|
|
||||||
|
|
@ -29,6 +30,7 @@ export default class DiscoursePostEventEvent {
|
||||||
@tracked status;
|
@tracked status;
|
||||||
@tracked post;
|
@tracked post;
|
||||||
@tracked minimal;
|
@tracked minimal;
|
||||||
|
@tracked chatEnabled;
|
||||||
@tracked canUpdateAttendance;
|
@tracked canUpdateAttendance;
|
||||||
@tracked canActOnDiscoursePostEvent;
|
@tracked canActOnDiscoursePostEvent;
|
||||||
@tracked shouldDisplayInvitees;
|
@tracked shouldDisplayInvitees;
|
||||||
|
|
@ -38,6 +40,7 @@ export default class DiscoursePostEventEvent {
|
||||||
@tracked recurrence;
|
@tracked recurrence;
|
||||||
@tracked recurrenceRule;
|
@tracked recurrenceRule;
|
||||||
@tracked customFields;
|
@tracked customFields;
|
||||||
|
@tracked channel;
|
||||||
|
|
||||||
@tracked _watchingInvitee;
|
@tracked _watchingInvitee;
|
||||||
@tracked _sampleInvitees;
|
@tracked _sampleInvitees;
|
||||||
|
|
@ -63,6 +66,7 @@ export default class DiscoursePostEventEvent {
|
||||||
this.isExpired = args.is_expired;
|
this.isExpired = args.is_expired;
|
||||||
this.isStandalone = args.is_standalone;
|
this.isStandalone = args.is_standalone;
|
||||||
this.minimal = args.minimal;
|
this.minimal = args.minimal;
|
||||||
|
this.chatEnabled = args.chat_enabled;
|
||||||
this.recurrenceRule = args.recurrence_rule;
|
this.recurrenceRule = args.recurrence_rule;
|
||||||
this.recurrence = args.recurrence;
|
this.recurrence = args.recurrence;
|
||||||
this.canUpdateAttendance = args.can_update_attendance;
|
this.canUpdateAttendance = args.can_update_attendance;
|
||||||
|
|
@ -72,6 +76,9 @@ export default class DiscoursePostEventEvent {
|
||||||
this.stats = args.stats;
|
this.stats = args.stats;
|
||||||
this.reminders = args.reminders;
|
this.reminders = args.reminders;
|
||||||
this.customFields = EmberObject.create(args.custom_fields || {});
|
this.customFields = EmberObject.create(args.custom_fields || {});
|
||||||
|
if (args.channel) {
|
||||||
|
this.channel = ChatChannel.create(args.channel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get watchingInvitee() {
|
get watchingInvitee() {
|
||||||
|
|
@ -138,6 +145,7 @@ export default class DiscoursePostEventEvent {
|
||||||
this.isExpired = event.isExpired;
|
this.isExpired = event.isExpired;
|
||||||
this.isStandalone = event.isStandalone;
|
this.isStandalone = event.isStandalone;
|
||||||
this.minimal = event.minimal;
|
this.minimal = event.minimal;
|
||||||
|
this.chatEnabled = event.chatEnabled;
|
||||||
this.recurrenceRule = event.recurrenceRule;
|
this.recurrenceRule = event.recurrenceRule;
|
||||||
this.recurrence = event.recurrence;
|
this.recurrence = event.recurrence;
|
||||||
this.canUpdateAttendance = event.canUpdateAttendance;
|
this.canUpdateAttendance = event.canUpdateAttendance;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ const EVENT_ATTRIBUTES = {
|
||||||
status: { default: "public" },
|
status: { default: "public" },
|
||||||
timezone: { default: "UTC" },
|
timezone: { default: "UTC" },
|
||||||
allowedGroups: { default: null },
|
allowedGroups: { default: null },
|
||||||
|
chatEnabled: { default: null },
|
||||||
|
chatChannelId: { default: null },
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {RichEditorExtension} */
|
/** @type {RichEditorExtension} */
|
||||||
|
|
|
||||||
|
|
@ -350,6 +350,7 @@ $show-interested: inherit;
|
||||||
|
|
||||||
.event-url,
|
.event-url,
|
||||||
.event-dates,
|
.event-dates,
|
||||||
|
.event-chat-channel,
|
||||||
.event-invitees-avatars-container {
|
.event-invitees-avatars-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 3em 1fr;
|
grid-template-columns: 3em 1fr;
|
||||||
|
|
@ -357,7 +358,7 @@ $show-interested: inherit;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: min-content;
|
height: min-content;
|
||||||
|
|
||||||
.d-icon {
|
> .d-icon {
|
||||||
color: var(--primary-high);
|
color: var(--primary-high);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
@ -398,12 +399,13 @@ $show-interested: inherit;
|
||||||
.event-invitees-icon {
|
.event-invitees-icon {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-invitees-icon .going {
|
.event-invitees-icon .going {
|
||||||
font-size: var(--font-down-3);
|
font-size: var(--font-down-3);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 4px;
|
right: -2px;
|
||||||
bottom: -8px;
|
bottom: -8px;
|
||||||
background: var(--secondary);
|
background: var(--secondary);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
|
||||||
|
|
@ -460,6 +460,9 @@ en:
|
||||||
minimal:
|
minimal:
|
||||||
label: "Minimal event"
|
label: "Minimal event"
|
||||||
checkbox_label: "Hide Going/Not going buttons and invitees status"
|
checkbox_label: "Hide Going/Not going buttons and invitees status"
|
||||||
|
allow_chat:
|
||||||
|
label: "Chat integration"
|
||||||
|
checkbox_label: "Create and manage event specific chat channel"
|
||||||
url:
|
url:
|
||||||
label: "URL"
|
label: "URL"
|
||||||
placeholder: "Optional"
|
placeholder: "Optional"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
class AddChatFieldsToEvents < ActiveRecord::Migration[7.2]
|
||||||
|
def change
|
||||||
|
add_column :discourse_post_event_events, :chat_enabled, :boolean, default: false, null: false
|
||||||
|
add_column :discourse_post_event_events, :chat_channel_id, :bigint
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -14,6 +14,7 @@ module DiscoursePostEvent
|
||||||
:timezone,
|
:timezone,
|
||||||
:minimal,
|
:minimal,
|
||||||
:closed,
|
:closed,
|
||||||
|
:"chat-enabled",
|
||||||
]
|
]
|
||||||
|
|
||||||
def self.extract_events(post)
|
def self.extract_events(post)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
return if !defined?(Chat)
|
||||||
|
|
||||||
|
describe DiscoursePostEvent::ChatChannelSync do
|
||||||
|
fab!(:user)
|
||||||
|
fab!(:admin)
|
||||||
|
fab!(:admin_post) { Fabricate(:post, user: admin) }
|
||||||
|
|
||||||
|
it "is able to create a chat channel and sync members" do
|
||||||
|
event = Fabricate(:event, chat_enabled: true, post: admin_post)
|
||||||
|
|
||||||
|
expect(event.chat_channel_id).to be_present
|
||||||
|
expect(event.chat_channel.name).to eq(event.name)
|
||||||
|
expect(event.chat_channel.user_chat_channel_memberships.count).to eq(1)
|
||||||
|
expect(event.chat_channel.user_chat_channel_memberships.first.user_id).to eq(admin.id)
|
||||||
|
|
||||||
|
event.create_invitees([user_id: user.id, status: DiscoursePostEvent::Invitee.statuses[:going]])
|
||||||
|
event.save!
|
||||||
|
|
||||||
|
expect(event.chat_channel.user_chat_channel_memberships.count).to eq(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "will simply do nothing if user has no permission to create channel" do
|
||||||
|
post = Fabricate(:post, user: user)
|
||||||
|
event = Fabricate(:event, chat_enabled: true, post: post)
|
||||||
|
|
||||||
|
expect(event.chat_channel_id).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -105,22 +105,25 @@ describe "Post event", type: :system do
|
||||||
it "persists changes" do
|
it "persists changes" do
|
||||||
visit "/new-topic"
|
visit "/new-topic"
|
||||||
composer.fill_title("Test event with updates")
|
composer.fill_title("Test event with updates")
|
||||||
page.find(".toolbar-popup-menu-options .dropdown-select-box-header").click
|
find(".toolbar-popup-menu-options .dropdown-select-box-header").click
|
||||||
page.find(
|
find(
|
||||||
".toolbar-popup-menu-options [data-name='#{I18n.t("js.discourse_post_event.builder_modal.attach")}']",
|
".toolbar-popup-menu-options [data-name='#{I18n.t("js.discourse_post_event.builder_modal.attach")}']",
|
||||||
).click
|
).click
|
||||||
page.find(".d-modal input[name=status][value=private]").click
|
find(".d-modal input[name=status][value=private]").click
|
||||||
page.find(".d-modal input.group-selector").send_keys("test_")
|
find(".d-modal input.group-selector").send_keys("test_")
|
||||||
page.find(".autocomplete.ac-group").click
|
find(".autocomplete.ac-group").click
|
||||||
page.find(".d-modal .custom-field-input").fill_in(with: "custom value")
|
find(".d-modal .custom-field-input").fill_in(with: "custom value")
|
||||||
page.find(".d-modal .btn-primary").click
|
find(".d-modal .btn-primary").click
|
||||||
composer.submit
|
|
||||||
page.find(".discourse-post-event-more-menu-trigger").click
|
|
||||||
page.find(".edit-event").click
|
|
||||||
|
|
||||||
expect(page.find(".d-modal input[name=status][value=private]").checked?).to eq(true)
|
expect(page).to have_no_css(".d-modal")
|
||||||
expect(page.find(".d-modal")).to have_text("test_group")
|
|
||||||
expect(page.find(".d-modal .custom-field-input").value).to eq("custom value")
|
composer.submit
|
||||||
|
find(".discourse-post-event-more-menu-trigger").click
|
||||||
|
find(".edit-event").click
|
||||||
|
|
||||||
|
expect(find(".d-modal input[name=status][value=private]").checked?).to eq(true)
|
||||||
|
expect(find(".d-modal")).to have_text("test_group")
|
||||||
|
expect(find(".d-modal .custom-field-input").value).to eq("custom value")
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when using bulk inline invite" do
|
context "when using bulk inline invite" do
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue