FEATURE: Option to disable user presence and profile

This allows users who are privacy conscious to disable the presence
features of the forum as well as their public profile.
This commit is contained in:
Robin Ward 2018-10-10 13:00:08 -04:00
parent fd48ba10b8
commit a566ed42ae
27 changed files with 178 additions and 23 deletions

View File

@ -31,7 +31,8 @@ export default Ember.Controller.extend(PreferencesTabController, {
"disable_jump_reply",
"automatically_unpin_topics",
"allow_private_messages",
"homepage_id"
"homepage_id",
"hide_profile_and_presence"
];
if (makeDefault) {

View File

@ -16,9 +16,9 @@ export default Ember.Controller.extend(CanCheckEmails, {
return currentUser && username === currentUser.get("username");
},
@computed("viewingSelf")
canExpandProfile(viewingSelf) {
return viewingSelf;
@computed("viewingSelf", "model.profile_hidden")
canExpandProfile(viewingSelf, profileHidden) {
return !profileHidden && viewingSelf;
},
@computed("model.profileBackground")
@ -26,8 +26,11 @@ export default Ember.Controller.extend(CanCheckEmails, {
return !Ember.isEmpty(background.toString());
},
@computed("indexStream", "viewingSelf", "forceExpand")
collapsedInfo(indexStream, viewingSelf, forceExpand) {
@computed("model.profile_hidden", "indexStream", "viewingSelf", "forceExpand")
collapsedInfo(profileHidden, indexStream, viewingSelf, forceExpand) {
if (profileHidden) {
return true;
}
return (!indexStream || viewingSelf) && !forceExpand;
},

View File

@ -284,7 +284,8 @@ const User = RestModel.extend({
"include_tl0_in_digests",
"theme_ids",
"allow_private_messages",
"homepage_id"
"homepage_id",
"hide_profile_and_presence"
];
if (fields) {

View File

@ -102,6 +102,7 @@ export default function() {
"user",
{ path: "/u/:username", resetNamespace: true },
function() {
this.route("profile-hidden");
this.route("summary");
this.route(
"userActivity",

View File

@ -1,6 +1,11 @@
export default Discourse.Route.extend({
model() {
return this.modelFor("user");
let user = this.modelFor("user");
if (user.get("profile_hidden")) {
return this.replaceWith("user.profile-hidden");
}
return user;
},
setupController(controller, user) {

View File

@ -2,7 +2,12 @@ export default Discourse.Route.extend({
showFooter: true,
model() {
return this.modelFor("user").summary();
let user = this.modelFor("user");
if (user.get("profile_hidden")) {
return this.replaceWith("user.profile-hidden");
}
return user.summary();
},
actions: {

View File

@ -37,10 +37,10 @@
{{preference-checkbox labelKey="user.enable_quoting" checked=model.user_option.enable_quoting}}
{{preference-checkbox labelKey="user.dynamic_favicon" checked=model.user_option.dynamic_favicon}}
{{preference-checkbox labelKey="user.disable_jump_reply" checked=model.user_option.disable_jump_reply}}
{{#if siteSettings.automatically_unpin_topics}}
{{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.user_option.automatically_unpin_topics}}
{{/if}}
{{preference-checkbox labelKey="user.hide_profile_and_presence" checked=model.user_option.hide_profile_and_presence}}
</div>
{{plugin-outlet name="user-preferences-interface" args=(hash model=model save=(action "save"))}}

View File

@ -195,8 +195,10 @@
</section>
{{#mobile-nav class='main-nav' desktopClass="nav nav-pills user-nav" currentPath=currentPath}}
<li>{{#link-to 'user.summary'}}{{i18n 'user.summary.title'}}{{/link-to}}</li>
<li>{{#link-to 'userActivity'}}{{i18n 'user.activity_stream'}}{{/link-to}}</li>
{{#unless model.profile_hidden}}
<li>{{#link-to 'user.summary'}}{{i18n 'user.summary.title'}}{{/link-to}}</li>
<li>{{#link-to 'userActivity'}}{{i18n 'user.activity_stream'}}{{/link-to}}</li>
{{/unless}}
{{#if showNotificationsTab}}
<li>
{{#link-to 'userNotifications'}}

View File

@ -0,0 +1 @@
<p class='user-profile-hidden'>{{i18n "user.profile_hidden"}}</p>

View File

@ -41,6 +41,10 @@
}
}
.user-profile-hidden {
font-size: 1.5em;
text-align: center;
}
.user-navigation {
display: table-cell;
vertical-align: top;

View File

@ -7,6 +7,8 @@ class UserActionsController < ApplicationController
per_chunk = 30
user = fetch_user_from_params(include_inactive: current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts))
raise Discourse::NotFound unless guardian.can_see_profile?(user)
action_types = (params[:filter] || "").split(",").map(&:to_i)
opts = { user_id: user.id,

View File

@ -53,14 +53,18 @@ class UsersController < ApplicationController
include_inactive: current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts)
)
user_serializer = UserSerializer.new(@user, scope: guardian, root: 'user')
user_serializer = nil
if guardian.can_see_profile?(@user)
user_serializer = UserSerializer.new(@user, scope: guardian, root: 'user')
# TODO remove this options from serializer
user_serializer.omit_stats = true
# TODO remove this options from serializer
user_serializer.omit_stats = true
topic_id = params[:include_post_count_for].to_i
if topic_id != 0
user_serializer.topic_post_count = { topic_id => Post.secured(guardian).where(topic_id: topic_id, user_id: @user.id).count }
topic_id = params[:include_post_count_for].to_i
if topic_id != 0
user_serializer.topic_post_count = { topic_id => Post.secured(guardian).where(topic_id: topic_id, user_id: @user.id).count }
end
else
user_serializer = HiddenProfileSerializer.new(@user, scope: guardian, root: 'user')
end
if !params[:skip_track_visit] && (@user != current_user)
@ -204,8 +208,15 @@ class UsersController < ApplicationController
end
end
def profile_hidden
render nothing: true
end
def summary
user = fetch_user_from_params(include_inactive: current_user.try(:staff?) || (current_user && SiteSetting.show_inactive_accounts))
raise Discourse::NotFound unless guardian.can_see_profile?(user)
summary = UserSummary.new(user, guardian)
serializer = UserSummarySerializer.new(summary, scope: guardian)
render_json_dump(serializer)

View File

@ -42,7 +42,8 @@ class CurrentUserSerializer < BasicUserSerializer
:can_create_topic,
:link_posting_access,
:external_id,
:top_category_ids
:top_category_ids,
:hide_profile_and_presence
def link_posting_access
scope.link_posting_access
@ -68,6 +69,10 @@ class CurrentUserSerializer < BasicUserSerializer
object.user_stat.topic_reply_count
end
def hide_profile_and_presence
object.user_option.hide_profile_and_presence
end
def enable_quoting
object.user_option.enable_quoting
end

View File

@ -0,0 +1,7 @@
class HiddenProfileSerializer < BasicUserSerializer
attributes :profile_hidden?
def profile_hidden?
true
end
end

View File

@ -23,6 +23,7 @@ class UserOptionSerializer < ApplicationSerializer
:theme_key_seq,
:allow_private_messages,
:homepage_id,
:hide_profile_and_presence
def auto_track_topics_after_msecs
object.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs

View File

@ -37,6 +37,7 @@ class UserUpdater
:theme_ids,
:allow_private_messages,
:homepage_id,
:hide_profile_and_presence
]
def initialize(actor, user)

View File

@ -618,6 +618,7 @@ en:
private_messages: "Messages"
activity_stream: "Activity"
preferences: "Preferences"
profile_hidden: "This user's public profile is hidden."
expand_profile: "Expand"
collapse_profile: "Collapse"
bookmarks: "Bookmarks"
@ -887,6 +888,7 @@ en:
website: "Web Site"
email_settings: "Email"
hide_profile_and_presence: "Hide my public profile and presence features"
like_notification_frequency:
title: "Notify when liked"
always: "Always"

View File

@ -432,6 +432,7 @@ Discourse::Application.routes.draw do
get "#{root_path}/:username/flagged-posts" => "users#show", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/deleted-posts" => "users#show", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/topic-tracking-state" => "users#topic_tracking_state", constraints: { username: RouteFormat.username }
get "#{root_path}/:username/profile-hidden" => "users#profile_hidden"
end
get "user-badges/:username.json" => "user_badges#username", constraints: { username: RouteFormat.username }, defaults: { format: :json }

View File

@ -0,0 +1,5 @@
class AddHideProfileAndPresenceToUserOptions < ActiveRecord::Migration[5.2]
def change
add_column :user_options, :hide_profile_and_presence, :boolean, default: false, null: false
end
end

View File

@ -93,4 +93,15 @@ module UserGuardian
user && can_administer_user?(user)
end
def can_see_profile?(user)
return false if user.blank?
# If a user has hidden their profile, restrict it to them and staff
if user.user_option.try(:hide_profile_and_presence?)
return is_me?(user) || is_staff?
end
true
end
end

View File

@ -113,6 +113,12 @@ export default Ember.Component.extend({
publish(data) {
this._lastPublish = new Date();
// Don't publish presence if disabled
if (this.currentUser.hide_profile_and_presence) {
return Ember.RSVP.Promise.resolve();
}
return ajax("/presence/publish", { type: "POST", data });
},

View File

@ -107,8 +107,7 @@ after_initialize do
ACTIONS ||= [-"edit", -"reply"].freeze
def publish
raise Discourse::NotFound if !current_user
raise Discourse::NotFound if current_user.blank? || current_user.user_option.hide_profile_and_presence?
data = params.permit(
:response_needed,

View File

@ -37,6 +37,12 @@ describe ::Presence::PresencesController do
expect { post '/presence/publish.json' }.not_to raise_error
end
it "does not publish for users with disabled presence features" do
user1.user_option.update_column(:hide_profile_and_presence, true)
post '/presence/publish.json'
expect(response.code).to eq("404")
end
it "uses guardian to secure endpoint" do
private_post = Fabricate(:private_message_post)

View File

@ -95,6 +95,41 @@ describe UserGuardian do
expect(guardian.can_pick_avatar?(user_avatar, nil)).to eq(true)
end
end
end
describe "#can_see_profile?" do
it "is false for no user" do
expect(Guardian.new.can_see_profile?(nil)).to eq(false)
end
it "is true for a user whose profile is public" do
expect(Guardian.new.can_see_profile?(user)).to eq(true)
end
context "hidden profile" do
let(:hidden_user) do
result = Fabricate(:user)
result.user_option.update_column(:hide_profile_and_presence, true)
result
end
it "is false for another user" do
expect(Guardian.new(user).can_see_profile?(hidden_user)).to eq(false)
end
it "is false for an anonymous user" do
expect(Guardian.new.can_see_profile?(hidden_user)).to eq(false)
end
it "is true for the user themselves" do
expect(Guardian.new(hidden_user).can_see_profile?(hidden_user)).to eq(true)
end
it "is true for a staff user" do
expect(Guardian.new(admin).can_see_profile?(hidden_user)).to eq(true)
end
end
end
end

View File

@ -27,7 +27,14 @@ describe UserOption do
user.user_option.expects(:redirected_to_top).returns(nil)
expect(user.user_option.should_be_redirected_to_top).to eq(false)
end
end
describe "defaults" do
let(:user) { Fabricate(:user) }
it "should not hide the profile and presence by default" do
expect(user.user_option.hide_profile_and_presence).to eq(false)
end
end
describe "#mailing_list_mode" do

View File

@ -9,6 +9,15 @@ describe UserActionsController do
expect(response.status).to eq(400)
end
it "returns a 404 for a user with a hidden profile" do
UserActionCreator.enable
post = Fabricate(:post)
post.user.user_option.update_column(:hide_profile_and_presence, true)
get "/user_actions.json", params: { username: post.user.username }
expect(response.code).to eq("404")
end
it 'renders list correctly' do
UserActionCreator.enable
post = Fabricate(:post)

View File

@ -2171,6 +2171,14 @@ describe UsersController do
expect(json["user_summary"]["topic_count"]).to eq(1)
expect(json["user_summary"]["post_count"]).to eq(0)
end
it "returns 404 for a hidden profile" do
user = Fabricate(:user)
user.user_option.update_column(:hide_profile_and_presence, true)
get "/u/#{user.username_lower}/summary.json"
expect(response.status).to eq(404)
end
end
describe '#confirm_admin' do
@ -2417,7 +2425,23 @@ describe UsersController do
it "returns success" do
get "/u/#{user.username}.json"
expect(response.status).to eq(200)
expect(JSON.parse(response.body)["user"]["username"]).to eq(user.username)
parsed = JSON.parse(response.body)["user"]
expect(parsed['username']).to eq(user.username)
expect(parsed["profile_hidden"]).to be_blank
expect(parsed["trust_level"]).to be_present
end
it "returns a hidden profile" do
user.user_option.update_column(:hide_profile_and_presence, true)
get "/u/#{user.username}.json"
expect(response.status).to eq(200)
parsed = JSON.parse(response.body)["user"]
expect(parsed["username"]).to eq(user.username)
expect(parsed["profile_hidden"]).to eq(true)
expect(parsed["trust_level"]).to be_blank
end
it "should redirect to login page for anonymous user when profiles are hidden" do