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