diff --git a/app/assets/javascripts/channels/synchronized_editor_channel.js b/app/assets/javascripts/channels/synchronized_editor_channel.js index affbd467..b87317c9 100644 --- a/app/assets/javascripts/channels/synchronized_editor_channel.js +++ b/app/assets/javascripts/channels/synchronized_editor_channel.js @@ -1,12 +1,26 @@ $(document).on('turbolinks:load', function () { if (window.location.pathname.includes('/implement')) { + function generateUUID() { + // We decided to use this function instead of crypto.randomUUID() because it also supports older browser versions + // https://caniuse.com/?search=createObjectURL + return URL.createObjectURL(new Blob()).slice(-36) + } + + function is_other_user(user) { + return !_.isEqual(current_user, user); + } + + function is_other_session(other_session_id) { + return session_id !== other_session_id; + } + const editor = $('#editor'); const exercise_id = editor.data('exercise-id'); - const current_user_id = editor.data('user-id'); const current_contributor_id = editor.data('contributor-id'); + const session_id = generateUUID(); - if ($.isController('exercises') && current_user_id !== current_contributor_id) { + if ($.isController('exercises') && current_user.id !== current_contributor_id) { App.synchronized_editor = App.cable.subscriptions.create({ channel: "SynchronizedEditorChannel", exercise_id: exercise_id @@ -23,25 +37,29 @@ $(document).on('turbolinks:load', function () { received(data) { // Called when there's incoming data on the websocket for this channel - if (current_user_id !== data.current_user_id) { - switch(data.command) { - case 'editor_change': - CodeOceanEditor.applyChanges(data.delta.data, data.active_file); - break; - case 'connection_change': - CodeOceanEditor.showPartnersConnectionStatus(data.status, data.current_user_name); - this.perform('send_hello'); - break; - case 'hello': - CodeOceanEditor.showPartnersConnectionStatus(data.status, data.current_user_name); - break; - } + switch (data.action) { + case 'editor_change': + if (is_other_session(data.session_id)) { + CodeOceanEditor.applyChanges(data.delta, data.active_file); + } + break; + case 'connection_change': + if (is_other_user(data.user)) { + CodeOceanEditor.showPartnersConnectionStatus(data.status, data.user.displayname); + this.perform('connection_status'); + } + break; + case 'connection_status': + if (is_other_user(data.user)) { + CodeOceanEditor.showPartnersConnectionStatus(data.status, data.user.displayname); + } + break; } }, - send_changes(delta, active_file) { - const delta_with_user_id = {command: 'editor_change', current_user_id: current_user_id, active_file: active_file, delta: delta} - this.perform('send_changes', {delta_with_user_id: delta_with_user_id}); + editor_change(delta, active_file) { + const message = {session_id: session_id, active_file: active_file, delta: delta.data} + this.perform('editor_change', message); }, is_connected() { diff --git a/app/assets/javascripts/editor/editor.js.erb b/app/assets/javascripts/editor/editor.js.erb index 5242da96..9d7ecb88 100644 --- a/app/assets/javascripts/editor/editor.js.erb +++ b/app/assets/javascripts/editor/editor.js.erb @@ -339,7 +339,7 @@ var CodeOceanEditor = { CodeOceanEditor.lastDeltaObject = null; return; } - App.synchronized_editor?.send_changes(deltaObject, this.active_file); + App.synchronized_editor?.editor_change(deltaObject, this.active_file); // TODO: This is a workaround for a bug in Ace. Remove when upgrading Ace. this.handleUTF16Surrogates(deltaObject, session); diff --git a/app/channels/synchronized_editor_channel.rb b/app/channels/synchronized_editor_channel.rb index d6a5f90c..04c03d9c 100644 --- a/app/channels/synchronized_editor_channel.rb +++ b/app/channels/synchronized_editor_channel.rb @@ -3,13 +3,19 @@ class SynchronizedEditorChannel < ApplicationCable::Channel def subscribed stream_from specific_channel - ActionCable.server.broadcast(specific_channel, {command: 'connection_change', status: 'connected', current_user_id: current_user.id, current_user_name: current_user.name}) + message = create_message('connection_change', 'connected') + + Event::SynchronizedEditor.create_for_connection_change(message, current_user, programming_group) + ActionCable.server.broadcast(specific_channel, message) end def unsubscribed # Any cleanup needed when channel is unsubscribed stop_all_streams - ActionCable.server.broadcast(specific_channel, {command: 'connection_change', status: 'disconnected', current_user_id: current_user.id, current_user_name: current_user.name}) + message = create_message('connection_change', 'disconnected') + + Event::SynchronizedEditor.create_for_connection_change(message, current_user, programming_group) + ActionCable.server.broadcast(specific_channel, message) end def specific_channel @@ -21,14 +27,22 @@ class SynchronizedEditorChannel < ApplicationCable::Channel current_contributor if current_contributor.programming_group? end - def send_changes(message) - change = message['delta_with_user_id'].deep_symbolize_keys + def editor_change(message) + change = message.deep_symbolize_keys Event::SynchronizedEditor.create_for_editor_change(change, current_user, programming_group) ActionCable.server.broadcast(specific_channel, change) end - def send_hello - ActionCable.server.broadcast(specific_channel, {command: 'hello', status: 'connected', current_user_id: current_user.id, current_user_name: current_user.name}) + def connection_status + ActionCable.server.broadcast(specific_channel, create_message('connection_status', 'connected')) + end + + def create_message(action, status) + { + action:, + status:, + user: current_user.to_page_context, + } end end diff --git a/app/models/event/synchronized_editor.rb b/app/models/event/synchronized_editor.rb index ce41a0be..57796f7a 100644 --- a/app/models/event/synchronized_editor.rb +++ b/app/models/event/synchronized_editor.rb @@ -7,53 +7,51 @@ class Event::SynchronizedEditor < ApplicationRecord belongs_to :programming_group belongs_to :study_group - belongs_to :file, class_name: 'CodeOcean::File' + belongs_to :file, class_name: 'CodeOcean::File', optional: true - enum command: { + enum action: { editor_change: 0, connection_change: 1, - hello: 2, - ### TODO: Kira's commands + connection_status: 2, }, _prefix: true enum status: { connected: 0, disconnected: 1, - ### TODO: connected, disconnected ... }, _prefix: true - enum action: { + enum editor_action: { insertText: 0, insertLines: 1, removeText: 2, removeLines: 3, changeFold: 4, removeFold: 5, - ### TODO: AceEditor Actions: insertText, insertLines, removeText, removesLines, ... }, _prefix: true - validates :status, presence: true, if: -> { command_connection_change? } - validates :action, presence: true, if: -> { command_editor_change? } - validates :range_start_row, numericality: {only_integer: true, greater_than_or_equal_to: 0}, if: -> { command_editor_change? } - validates :range_start_column, numericality: {only_integer: true, greater_than_or_equal_to: 0}, if: -> { command_editor_change? } - validates :range_end_row, numericality: {only_integer: true, greater_than_or_equal_to: 0}, if: -> { command_editor_change? } - validates :range_end_column, numericality: {only_integer: true, greater_than_or_equal_to: 0}, if: -> { command_editor_change? } - validates :nl, inclusion: {in: %W[\n \r\n]}, if: -> { action_removeLines? } + validates :status, presence: true, if: -> { action_connection_change? } + validates :file_id, presence: true, if: -> { action_editor_change? } + validates :editor_action, presence: true, if: -> { action_editor_change? } + validates :range_start_row, numericality: {only_integer: true, greater_than_or_equal_to: 0}, if: -> { action_editor_change? } + validates :range_start_column, numericality: {only_integer: true, greater_than_or_equal_to: 0}, if: -> { action_editor_change? } + validates :range_end_row, numericality: {only_integer: true, greater_than_or_equal_to: 0}, if: -> { action_editor_change? } + validates :range_end_column, numericality: {only_integer: true, greater_than_or_equal_to: 0}, if: -> { action_editor_change? } + validates :nl, inclusion: {in: %W[\n \r\n]}, if: -> { editor_action_removeLines? } validate :either_lines_or_text def self.create_for_editor_change(event, user, programming_group) event_copy = event.deep_dup file = event_copy.delete(:active_file) - delta = event_copy.delete(:delta)[:data] + delta = event_copy.delete(:delta) range = delta.delete(:range) create!( user:, programming_group:, study_group_id: user.current_study_group_id, - command: event_copy.delete(:command), - action: delta.delete(:action), + action: event_copy.delete(:action), + editor_action: delta.delete(:action), file_id: file[:id], range_start_row: range[:start][:row], range_start_column: range[:start][:column], @@ -66,9 +64,19 @@ class Event::SynchronizedEditor < ApplicationRecord ) end + def self.create_for_connection_change(message, user, programming_group) + create!( + user:, + programming_group:, + study_group_id: user.current_study_group_id, + action: message[:action], + status: message[:status] + ) + end + def self.data_attribute(event, delta) event[:delta] = {data: delta} if delta.present? - event.presence if event.present? # TODO: As of now, we are storing the `current_user_id` most of the times. Intended? + event.presence if event.present? # TODO: As of now, we are storing the `session_id` most of the times. Intended? end private_class_method :data_attribute diff --git a/app/views/exercises/_editor.html.slim b/app/views/exercises/_editor.html.slim index e5475e7c..57578e0f 100644 --- a/app/views/exercises/_editor.html.slim +++ b/app/views/exercises/_editor.html.slim @@ -4,7 +4,7 @@ - show_tips_interventions = @show_tips_interventions || "false" - hide_rfc_button = @hide_rfc_button || false -#editor.row data-exercise-id=@exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-message-out-of-memory=t('exercises.editor.out_of_memory', memory_limit: @exercise.execution_environment.memory_limit) data-submissions-url=submissions_path data-user-id=current_user.id data-user-name=current_user.name data-contributor-id=current_contributor.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path(@exercise) data-intervention-save-url=intervention_exercise_path(@exercise) data-rfc-interventions=show_rfc_interventions data-break-interventions=show_break_interventions data-tips-interventions=show_tips_interventions +#editor.row data-exercise-id=@exercise.id data-message-depleted=t('exercises.editor.depleted') data-message-timeout=t('exercises.editor.timeout', permitted_execution_time: @exercise.execution_environment.permitted_execution_time) data-message-out-of-memory=t('exercises.editor.out_of_memory', memory_limit: @exercise.execution_environment.memory_limit) data-submissions-url=submissions_path data-contributor-id=current_contributor.id data-user-external-id=external_user_external_id data-working-times-url=working_times_exercise_path(@exercise) data-intervention-save-url=intervention_exercise_path(@exercise) data-rfc-interventions=show_rfc_interventions data-break-interventions=show_break_interventions data-tips-interventions=show_tips_interventions - unless @embed_options[:hide_sidebar] - additional_classes = 'sidebar-col' - if @tips.blank? diff --git a/db/migrate/20230906163923_rename_columns_in_events_synchronized_editor.rb b/db/migrate/20230906163923_rename_columns_in_events_synchronized_editor.rb new file mode 100644 index 00000000..f6b090d7 --- /dev/null +++ b/db/migrate/20230906163923_rename_columns_in_events_synchronized_editor.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class RenameColumnsInEventsSynchronizedEditor < ActiveRecord::Migration[7.0] + def change + change_table :events_synchronized_editor do |t| + t.rename :action, :editor_action + t.rename :command, :action + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 0b16392c..8225c522 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_09_05_190519) do +ActiveRecord::Schema[7.0].define(version: 2023_09_06_163923) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" enable_extension "pgcrypto" @@ -154,10 +154,10 @@ ActiveRecord::Schema[7.0].define(version: 2023_09_05_190519) do t.bigint "study_group_id", null: false t.string "user_type", null: false t.bigint "user_id", null: false - t.integer "command", limit: 2, default: 0, null: false, comment: "Used as enum in Rails" + t.integer "action", limit: 2, default: 0, null: false, comment: "Used as enum in Rails" t.integer "status", limit: 2, comment: "Used as enum in Rails" t.bigint "file_id" - t.integer "action", limit: 2, comment: "Used as enum in Rails" + t.integer "editor_action", limit: 2, comment: "Used as enum in Rails" t.integer "range_start_row" t.integer "range_start_column" t.integer "range_end_row"