diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index 3970f877..1582404e 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -455,19 +455,6 @@ z-index: 4; } -.takeScreenshot { - margin-bottom: 5px; - border-radius: 2px; - background-image: url(<%= asset_path 'screenshot_sprite.png' %>); - display: none; -} -.takeScreenshot:hover { - background-position: -32px 0; -} -.canEditMap .takeScreenshot { - display: block; -} - .zoomExtents { margin-bottom:5px; border-radius: 2px; @@ -478,7 +465,7 @@ background-position: -32px 0; } -.zoomExtents:hover .tooltips, .zoomIn:hover .tooltips, .zoomOut:hover .tooltips, .takeScreenshot:hover .tooltips, .sidebarFilterIcon:hover .tooltipsUnder, .sidebarForkIcon:hover .tooltipsUnder, .notificationsIcon:hover .tooltipsUnder, .addMap:hover .tooltipsUnder, .authenticated .sidebarAccountIcon:hover .tooltipsUnder, +.zoomExtents:hover .tooltips, .zoomIn:hover .tooltips, .zoomOut:hover .tooltips, .sidebarFilterIcon:hover .tooltipsUnder, .sidebarForkIcon:hover .tooltipsUnder, .notificationsIcon:hover .tooltipsUnder, .addMap:hover .tooltipsUnder, .authenticated .sidebarAccountIcon:hover .tooltipsUnder, .mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove, .chat-button:hover .tooltips, .importDialog:hover .tooltipsUnder, .starMap:hover .tooltipsAbove, .openMetacodeSwitcher:hover .tooltipsAbove, .pinCarousel:not(.isPinned):hover .tooltipsAbove.helpPin, .pinCarousel.isPinned:hover .tooltipsAbove.helpUnpin { display: block; } @@ -609,7 +596,7 @@ margin-top: 40px; } -.zoomExtents div::after, .zoomIn div::after, .zoomOut div::after, .takeScreenshot div:after, .chat-button div.tooltips::after { +.zoomExtents div::after, .zoomIn div::after, .zoomOut div::after, .chat-button div.tooltips::after { content: ''; position: absolute; top: 57%; diff --git a/app/controllers/api/v2/users_controller.rb b/app/controllers/api/v2/users_controller.rb index 3f60c410..092a95b5 100644 --- a/app/controllers/api/v2/users_controller.rb +++ b/app/controllers/api/v2/users_controller.rb @@ -3,6 +3,7 @@ module Api module V2 class UsersController < RestfulController def current + raise Pundit::NotAuthorizedError if current_user.nil? @user = current_user authorize @user show # delegate to the normal show function diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 7aff655d..9bf10ac7 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -23,11 +23,12 @@ class UsersController < ApplicationController if user_params[:password] == '' && user_params[:password_confirmation] == '' # not trying to change the password if @user.update_attributes(user_params.except(:password, :password_confirmation)) + update_follow_settings(@user, params[:settings]) @user.image = nil if params[:remove_image] == '1' @user.save sign_in(@user, bypass: true) respond_to do |format| - format.html { redirect_to root_url, notice: 'Account updated!' } + format.html { redirect_to root_url, notice: 'Settings updated' } end else sign_in(@user, bypass: true) @@ -40,11 +41,12 @@ class UsersController < ApplicationController correct_pass = @user.valid_password?(params[:current_password]) if correct_pass && @user.update_attributes(user_params) + update_follow_settings(@user, params[:settings]) @user.image = nil if params[:remove_image] == '1' @user.save sign_in(@user, bypass: true) respond_to do |format| - format.html { redirect_to root_url, notice: 'Account updated!' } + format.html { redirect_to root_url, notice: 'Settings updated' } end else respond_to do |format| @@ -104,9 +106,16 @@ class UsersController < ApplicationController private + def update_follow_settings(user, settings) + user.settings.follow_topic_on_created = settings[:follow_topic_on_created] + user.settings.follow_topic_on_contributed = settings[:follow_topic_on_contributed] + user.settings.follow_map_on_created = settings[:follow_map_on_created] + user.settings.follow_map_on_contributed = settings[:follow_map_on_contributed] + end + def user_params params.require(:user).permit( - :name, :email, :image, :password, :password_confirmation, :emails_allowed + :name, :email, :image, :password, :password_confirmation, :emails_allowed, :settings ) end end diff --git a/app/models/map.rb b/app/models/map.rb index a149a760..dd5e5604 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -39,7 +39,7 @@ class Map < ApplicationRecord # Validate the attached image is image/jpg, image/png, etc validates_attachment_content_type :screenshot, content_type: %r{\Aimage/.*\Z} - after_create :after_created_async + after_create :after_created after_update :after_updated after_save :update_deferring_topics_and_synapses, if: :permission_changed? @@ -140,11 +140,10 @@ class Map < ApplicationRecord protected - def after_created_async + def after_created FollowService.follow(self, self.user, 'created') # notify users following the map creator end - handle_asynchronously :after_created_async def after_updated return unless ATTRS_TO_WATCH.any? { |k| changed_attributes.key?(k) } diff --git a/app/models/user.rb b/app/models/user.rb index 33ddc8d3..bb22f972 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -62,6 +62,12 @@ class User < ApplicationRecord maps: following.where(followed_type: 'Map').to_a.map(&:followed_id) } end + if (_options[:follow_settings]) + json['follow_topic_on_created'] = settings.follow_topic_on_created == "1" + json['follow_topic_on_contributed'] = settings.follow_topic_on_contributed == "1" + json['follow_map_on_created'] = settings.follow_map_on_created == "1" + json['follow_map_on_contributed'] = settings.follow_map_on_contributed == "1" + end if (_options[:email]) json['email'] = email end @@ -126,9 +132,23 @@ class User < ApplicationRecord stars.where(map_id: map.id).exists? end + def has_map_open(map) + latestEvent = Event.where(map: map, user: self) + .where(kind: ['user_present_on_map', 'user_not_present_on_map']) + .order(:created_at) + .last + latestEvent && latestEvent.kind == 'user_present_on_map' + end + + def has_map_with_synapse_open(synapse) + synapse.maps.any?{|map| has_map_open(map)} + end + def settings - # make sure we always return a UserPreference instance self[:settings] = UserPreference.new if self[:settings].nil? + if not self[:settings].respond_to?(:follow_topic_on_created) + self[:settings].initialize_follow_settings + end self[:settings] end diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 9ea37532..c881c4ac 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class UserPreference - attr_accessor :metacodes, :metacode_focus + attr_accessor :metacodes, :metacode_focus, :follow_topic_on_created, :follow_topic_on_contributed, + :follow_map_on_created, :follow_map_on_contributed def initialize array = [] @@ -16,5 +17,13 @@ class UserPreference end @metacodes = array @metacode_focus = array[0] + initialize_follow_settings + end + + def initialize_follow_settings + @follow_topic_on_created = false + @follow_topic_on_contributed = false + @follow_map_on_created = false + @follow_map_on_contributed = false end end diff --git a/app/serializers/api/v2/application_serializer.rb b/app/serializers/api/v2/application_serializer.rb index 81772577..c0399db6 100644 --- a/app/serializers/api/v2/application_serializer.rb +++ b/app/serializers/api/v2/application_serializer.rb @@ -13,50 +13,49 @@ module Api @embeds ||= (scope[:embeds] || []).select { |e| self.class.embeddable.keys.include?(e) } end - # self.embeddable might look like this: - # creator: { attr: :first_creator, serializer: UserSerializer } - # contributors: { serializer: UserSerializer} - # This method will remove the :attr key if the underlying attribute name - # is different than the name provided in the final json output. All other keys - # in the hash will be passed to the ActiveModel::Serializer `attribute` method - # directly (e.g. serializer in the examples will be passed). - # - # This setup means if you passed this self.embeddable config and sent no - # ?embed= query param with your API request, you would get the regular attributes - # plus creator_id and contributor_ids. If you passed ?embed=creator,contributors - # then instead of an id and an array of ids, you would get a serialized user - # (the first_creator) and an array of serialized users (the contributors). + # Here's an example object that could be passed in self.embeddable: { + # creator: { + # serializer: UserSerializer, + # }, + # collaborators: { + # serializer: UserSerializer + # }, + # topic: {}, + # synapses: {} + # } + # The key has to be in embeddable or it won't show in the response, and the serializer is + # only needed if the key doesn't match a serializer def self.embed_dat embeddable.each_pair do |key, opts| - attr = opts.delete(:attr) || key - if attr.to_s.pluralize == attr.to_s - attribute("#{attr.to_s.singularize}_ids".to_sym, - opts.merge(unless: -> { embeds.include?(key) })) do - Pundit.policy_scope(scope[:current_user], object.send(attr))&.map(&:id) || [] + is_plural = key.to_s.pluralize == key.to_s + id_key = key.to_s.singularize + (is_plural ? '_ids' : '_id') + serializer = opts.delete(:serializer) || "Api::V2::#{key.to_s.singularize.camelize}Serializer".constantize + if is_plural + attribute(id_key.to_sym, opts.merge(unless: -> { embeds.include?(key) })) do + Pundit.policy_scope(scope[:current_user], object.send(key))&.map(&:id) || [] end - has_many(attr, opts.merge(if: -> { embeds.include?(key) })) do - list = Pundit.policy_scope(scope[:current_user], object.send(attr)) || [] - child_serializer = "Api::V2::#{attr.to_s.singularize.camelize}Serializer".constantize + has_many(key, opts.merge(if: -> { embeds.include?(key) })) do + list = Pundit.policy_scope(scope[:current_user], object.send(key)) || [] resource = ActiveModelSerializers::SerializableResource.new( list, - each_serializer: child_serializer, + each_serializer: serializer, scope: scope.merge(embeds: []) ) - resource.as_json + # resource.as_json will return e.g. { users: [ ... ] } for collaborators + # since we can't get the :users key, convert to an array and use .first.second to get the needed values + resource&.as_json&.to_a&.first&.second end else - id_opts = opts.merge(key: "#{key}_id") - attribute("#{attr}_id".to_sym, - id_opts.merge(unless: -> { embeds.include?(key) })) - attribute(key, opts.merge(if: -> { embeds.include?(key) })) do |serializer| - object = serializer.object.send(key) - child_serializer = "Api::V2::#{object.class.name}Serializer".constantize + attribute(id_key.to_sym, opts.merge(unless: -> { embeds.include?(key) })) + attribute(key, opts.merge(if: -> { embeds.include?(key) })) do |parent_serializer| + object = parent_serializer.object.send(key) + next nil if object.nil? resource = ActiveModelSerializers::SerializableResource.new( object, - serializer: child_serializer, + serializer: serializer, scope: scope.merge(embeds: []) ) - resource.as_json + resource&.as_json&.to_a&.first&.second end end end diff --git a/app/serializers/api/v2/map_serializer.rb b/app/serializers/api/v2/map_serializer.rb index 7e090d33..3ba158f9 100644 --- a/app/serializers/api/v2/map_serializer.rb +++ b/app/serializers/api/v2/map_serializer.rb @@ -18,7 +18,7 @@ module Api def self.embeddable { user: {}, - source: {}, + source: { serializer: MapSerializer }, topics: {}, synapses: {}, mappings: {}, diff --git a/app/serializers/api/v2/mapping_serializer.rb b/app/serializers/api/v2/mapping_serializer.rb index 30c9bd7f..d418b31c 100644 --- a/app/serializers/api/v2/mapping_serializer.rb +++ b/app/serializers/api/v2/mapping_serializer.rb @@ -14,7 +14,7 @@ module Api def self.embeddable { user: {}, - updated_by: {}, + updated_by: { serializer: UserSerializer }, map: {} } end diff --git a/app/services/follow_service.rb b/app/services/follow_service.rb index 53add1cf..8434baca 100644 --- a/app/services/follow_service.rb +++ b/app/services/follow_service.rb @@ -3,7 +3,9 @@ class FollowService class << self def follow(entity, user, reason) - return unless is_tester(user) + return unless user && is_tester(user) + + return if (reason == 'created' || reason == 'contributed') && !should_auto_follow(entity, user, reason) follow = Follow.where(followed: entity, user: user).first_or_create if FollowReason::REASONS.include?(reason) && !follow.follow_reason.read_attribute(reason) @@ -28,8 +30,20 @@ class FollowService protected - def is_tester(user) - %w(connorturland@gmail.com devin@callysto.com chessscholar@gmail.com solaureum@gmail.com ishanshapiro@gmail.com).include?(user.email) + def should_auto_follow(entity, user, reason) + if entity.class == Topic + if reason == 'created' + return user.settings.follow_topic_on_created == '1' + elsif reason == 'contributed' + return user.settings.follow_topic_on_contributed == '1' + end + elsif entity.class == Map + if reason == 'created' + return user.settings.follow_map_on_created == '1' + elsif reason == 'contributed' + return user.settings.follow_map_on_contributed == '1' + end + end end end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 177489e1..c587e9ed 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -34,6 +34,14 @@ class NotificationService # we'll prbly want to put the body into the actual loop so we can pass the current user in as a local body = renderer.render(template: settings[:template], locals: { entity: entity, event: event }, layout: false) follows.each{|follow| + case event_type + when TOPIC_ADDED_TO_MAP + next unless TopicPolicy.new(follow.user, entity).show? && MapPolicy.new(follow.user, event.map).show? + next if follow.user.has_map_open(event.map) + when TOPIC_CONNECTED_1, TOPIC_CONNECTED_2 + next unless SynapsePolicy.new(follow.user, event).show? + next if follow.user.has_map_with_synapse_open(event) + end # this handles email and in-app notifications, in the future, include push follow.user.notify(settings[:subject], body, event, false, event_type, follow.user.emails_allowed, event.user) # push could be handled with Actioncable to send transient notifications to the UI diff --git a/app/views/layouts/_account.html.erb b/app/views/layouts/_account.html.erb index 3d66f687..94f69f62 100644 --- a/app/views/layouts/_account.html.erb +++ b/app/views/layouts/_account.html.erb @@ -10,7 +10,7 @@