From 2ee721f8aa303ae3e4b49e84ca882813b777b863 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 27 Sep 2022 22:06:40 +0530 Subject: [PATCH] FEATURE: add composer warning when user haven't been seen in a long time (#18340) * FEATURE: add composer warning when user haven't been seen in a long time When a user creates a PM and adds a recipient that hasn't been seen in a long time then we'll now show a warning in composer indicating that the user hasn't been seen in a long time. --- .../app/components/composer-messages.js | 50 +++++++++++++++++++ .../acceptance/composer-messages-test.js | 41 +++++++++++++++ .../composer_messages_controller.rb | 18 +++++++ config/locales/client.en.yml | 3 ++ config/locales/server.en.yml | 2 + config/routes.rb | 1 + config/site_settings.yml | 1 + lib/composer_messages_finder.rb | 4 ++ spec/lib/composer_messages_finder_spec.rb | 22 ++++++++ .../composer_messages_controller_spec.rb | 41 +++++++++++++++ 10 files changed, 183 insertions(+) create mode 100644 app/assets/javascripts/discourse/tests/acceptance/composer-messages-test.js diff --git a/app/assets/javascripts/discourse/app/components/composer-messages.js b/app/assets/javascripts/discourse/app/components/composer-messages.js index 45aa08ed95f..40832b5df17 100644 --- a/app/assets/javascripts/discourse/app/components/composer-messages.js +++ b/app/assets/javascripts/discourse/app/components/composer-messages.js @@ -5,8 +5,10 @@ import LinkLookup from "discourse/lib/link-lookup"; import { not } from "@ember/object/computed"; import { scheduleOnce } from "@ember/runloop"; import showModal from "discourse/lib/show-modal"; +import { ajax } from "discourse/lib/ajax"; let _messagesCache = {}; +let _recipient_names = []; export default Component.extend({ classNameBindings: [":composer-popup-container", "hidden"], @@ -18,6 +20,7 @@ export default Component.extend({ _similarTopicsMessage: null, _yourselfConfirm: null, similarTopics: null, + usersNotSeen: null, hidden: not("composer.viewOpenOrFullscreen"), @@ -119,6 +122,53 @@ export default Component.extend({ const composer = this.composer; if (composer.get("privateMessage")) { const recipients = composer.targetRecipientsArray; + const recipient_names = recipients + .filter((r) => r.type === "user") + .map(({ name }) => name); + + if ( + recipient_names.length > 0 && + recipient_names.length !== _recipient_names.length && + !recipient_names.every((v, i) => v === _recipient_names[i]) + ) { + _recipient_names = recipient_names; + + ajax(`/composer_messages/user_not_seen_in_a_while`, { + type: "GET", + data: { + usernames: recipient_names, + }, + }).then((response) => { + if ( + response.user_count > 0 && + this.get("usersNotSeen") !== response.usernames.join("-") + ) { + this.set("usersNotSeen", response.usernames.join("-")); + this.messagesByTemplate["education"] = undefined; + + let usernames = []; + response.usernames.forEach((username, index) => { + usernames[ + index + ] = `@${username}`; + }); + + let body_key = "composer.user_not_seen_in_a_while.single"; + if (response.user_count > 1) { + body_key = "composer.user_not_seen_in_a_while.multiple"; + } + const message = composer.store.createRecord("composer-message", { + id: "user-not-seen", + templateName: "education", + body: I18n.t(body_key, { + usernames: usernames.join(", "), + time_ago: response.time_ago, + }), + }); + this.send("popup", message); + } + }); + } if ( recipients.length > 0 && diff --git a/app/assets/javascripts/discourse/tests/acceptance/composer-messages-test.js b/app/assets/javascripts/discourse/tests/acceptance/composer-messages-test.js new file mode 100644 index 00000000000..25f9cae229e --- /dev/null +++ b/app/assets/javascripts/discourse/tests/acceptance/composer-messages-test.js @@ -0,0 +1,41 @@ +import { + acceptance, + exists, + query, +} from "discourse/tests/helpers/qunit-helpers"; +import { click, triggerKeyEvent, visit } from "@ember/test-helpers"; +import { test } from "qunit"; +import I18n from "I18n"; + +acceptance("Composer - Messages", function (needs) { + needs.user(); + needs.pretender((server, helper) => { + server.get("/composer_messages/user_not_seen_in_a_while", () => { + return helper.response({ + user_count: 1, + usernames: ["charlie"], + time_ago: "1 year ago", + }); + }); + }); + + test("Shows warning in composer if user hasn't been seen in a long time.", async function (assert) { + await visit("/u/charlie"); + await click("button.compose-pm"); + assert.ok( + !exists(".composer-popup"), + "composer warning is not shown by default" + ); + await triggerKeyEvent(".d-editor-input", "keyup", "Space"); + assert.ok(exists(".composer-popup"), "shows composer warning message"); + assert.ok( + query(".composer-popup").innerHTML.includes( + I18n.t("composer.user_not_seen_in_a_while.single", { + usernames: ['@charlie'], + time_ago: "1 year ago", + }) + ), + "warning message has correct body" + ); + }); +}); diff --git a/app/controllers/composer_messages_controller.rb b/app/controllers/composer_messages_controller.rb index c5d4de3c647..b8303014559 100644 --- a/app/controllers/composer_messages_controller.rb +++ b/app/controllers/composer_messages_controller.rb @@ -17,4 +17,22 @@ class ComposerMessagesController < ApplicationController render_json_dump(json, rest_serializer: true) end + + def user_not_seen_in_a_while + usernames = params.require(:usernames) + users = ComposerMessagesFinder.user_not_seen_in_a_while(usernames) + user_count = users.count + warning_message = nil + + if user_count > 0 + message_locale = if user_count == 1 + "education.user_not_seen_in_a_while.single" + else + "education.user_not_seen_in_a_while.multiple" + end + end + + json = { user_count: user_count, usernames: users, time_ago: FreedomPatches::Rails4.time_ago_in_words(SiteSetting.pm_warn_user_last_seen_months_ago.month.ago, true, scope: :'datetime.distance_in_words_verbose') } + render_json_dump(json) + end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 5789279a7d2..9159b3cbc78 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2275,6 +2275,9 @@ en: body: "Right now this message is only being sent to yourself!" slow_mode: error: "This topic is in slow mode. You already posted recently; you can post again in %{timeLeft}." + user_not_seen_in_a_while: + single: "The person you are messaging, %{usernames}, hasn’t been seen here in a very long time – %{time_ago}. They may not receive your message. You may wish to seek out alternate methods of contacting %{usernames}." + multiple: "The following people you are messaging: %{usernames}, haven’t been seen here in a very long time – %{time_ago}. They may not receive your message. You may wish to seek out alternate methods of contacting them." admin_options_title: "Optional staff settings for this topic" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index ea49d6f106a..5558bc40d26 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2149,6 +2149,8 @@ en: disable_avatar_education_message: "Disable education message for changing avatar." + pm_warn_user_last_seen_months_ago: "When creating a new PM warn users when target recepient has not been seen more than n months ago." + suppress_uncategorized_badge: "Don't show the badge for uncategorized topics in topic lists." header_dropdown_category_count: "How many categories can be displayed in the header dropdown menu." diff --git a/config/routes.rb b/config/routes.rb index f5eccd9af5e..6b9e4958f28 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -387,6 +387,7 @@ Discourse::Application.routes.draw do end get "session/scopes" => "session#scopes" get "composer_messages" => "composer_messages#index" + get "composer_messages/user_not_seen_in_a_while" => "composer_messages#user_not_seen_in_a_while" resources :static post "login" => "static#enter" diff --git a/config/site_settings.yml b/config/site_settings.yml index 93fb4a5f415..f2106b26382 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -2272,6 +2272,7 @@ uncategorized: get_a_room_threshold: 3 dominating_topic_minimum_percent: 20 disable_avatar_education_message: false + pm_warn_user_last_seen_months_ago: 24 global_notice: default: "" diff --git a/lib/composer_messages_finder.rb b/lib/composer_messages_finder.rb index 06ce89634e0..30acc8c7cda 100644 --- a/lib/composer_messages_finder.rb +++ b/lib/composer_messages_finder.rb @@ -212,6 +212,10 @@ class ComposerMessagesFinder } end + def self.user_not_seen_in_a_while(usernames) + User.where(username_lower: usernames).where("last_seen_at < ?", SiteSetting.pm_warn_user_last_seen_months_ago.months.ago).pluck(:username).sort + end + private def educate_reply?(type) diff --git a/spec/lib/composer_messages_finder_spec.rb b/spec/lib/composer_messages_finder_spec.rb index e6bbac2144c..7ff75f30f34 100644 --- a/spec/lib/composer_messages_finder_spec.rb +++ b/spec/lib/composer_messages_finder_spec.rb @@ -501,4 +501,26 @@ RSpec.describe ComposerMessagesFinder do expect(edit_post_finder.find).to eq(nil) end end + + describe '#user_not_seen_in_a_while' do + fab!(:user_1) { Fabricate(:user, last_seen_at: 3.years.ago) } + fab!(:user_2) { Fabricate(:user, last_seen_at: 2.years.ago) } + fab!(:user_3) { Fabricate(:user, last_seen_at: 6.months.ago) } + + before do + SiteSetting.pm_warn_user_last_seen_months_ago = 24 + end + + it 'returns users that have not been seen recently' do + users = ComposerMessagesFinder.user_not_seen_in_a_while([user_1.username, user_2.username, user_3.username]) + expect(users).to contain_exactly(user_1.username, user_2.username) + end + + it 'accounts for pm_warn_user_last_seen_months_ago site setting' do + SiteSetting.pm_warn_user_last_seen_months_ago = 30 + users = ComposerMessagesFinder.user_not_seen_in_a_while([user_1.username, user_2.username, user_3.username]) + expect(users).to contain_exactly(user_1.username) + end + end + end diff --git a/spec/requests/composer_messages_controller_spec.rb b/spec/requests/composer_messages_controller_spec.rb index 3ce4fe613da..22af26f4563 100644 --- a/spec/requests/composer_messages_controller_spec.rb +++ b/spec/requests/composer_messages_controller_spec.rb @@ -30,4 +30,45 @@ RSpec.describe ComposerMessagesController do end end end + + describe '#user_not_seen_in_a_while' do + fab!(:user_1) { Fabricate(:user, last_seen_at: 3.years.ago) } + fab!(:user_2) { Fabricate(:user, last_seen_at: 2.years.ago) } + fab!(:user_3) { Fabricate(:user, last_seen_at: 6.months.ago) } + + it 'requires you to be logged in' do + get '/composer_messages/user_not_seen_in_a_while.json', params: { usernames: [user_1.username, user_2.username, user_3.username] } + expect(response.status).to eq(403) + end + + context 'when logged in' do + let!(:user) { sign_in(Fabricate(:user)) } + + before do + SiteSetting.pm_warn_user_last_seen_months_ago = 24 + end + + it 'requires usernames parameter to be present' do + get '/composer_messages/user_not_seen_in_a_while.json' + expect(response.status).to eq(400) + end + + it 'returns users that have not been seen recently' do + get '/composer_messages/user_not_seen_in_a_while.json', params: { usernames: [user_1.username, user_2.username, user_3.username] } + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["user_count"]).to eq(2) + expect(json["usernames"]).to contain_exactly(user_1.username, user_2.username) + end + + it 'accounts for pm_warn_user_last_seen_months_ago site setting' do + SiteSetting.pm_warn_user_last_seen_months_ago = 30 + get '/composer_messages/user_not_seen_in_a_while.json', params: { usernames: [user_1.username, user_2.username, user_3.username] } + expect(response.status).to eq(200) + json = response.parsed_body + expect(json["user_count"]).to eq(1) + expect(json["usernames"]).to contain_exactly(user_1.username) + end + end + end end