From aed2d3b0e3d85ad1416120c665e22d8a8ccb2225 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Mon, 30 Oct 2023 08:27:14 +0800 Subject: [PATCH 01/71] wip --- .../category_controller.rb | 68 +++++ app/models/discourse_activity_pub_actor.rb | 6 +- app/models/discourse_activity_pub_follow.rb | 4 + .../follower_serializer.rb | 21 ++ .../activity-pub-category-route-map.js | 5 + .../activity-pub-category-banner.hbs | 8 + .../activity-pub-category-banner.js | 6 + .../components/activity-pub-discovery.hbs | 2 +- .../components/activity-pub-follow-btn.hbs | 8 + .../components/activity-pub-follow-btn.js | 13 + .../components/activity-pub-follow-domain.hbs | 26 ++ .../components/activity-pub-follow-domain.js | 90 +++++++ .../components/activity-pub-follower-card.hbs | 32 +++ .../components/activity-pub-follower-card.js | 21 ++ .../components/activity-pub-followers-btn.hbs | 6 + .../components/activity-pub-followers-btn.js | 33 +++ .../components/activity-pub-followers.hbs | 5 + .../components/activity-pub-followers.js | 3 + .../components/activity-pub-handle.hbs | 38 ++- .../components/activity-pub-handle.js | 11 +- .../components/activity-pub-statistics.hbs | 8 + .../components/activity-pub-status.hbs | 12 + .../components/modal/activity-pub-follow.hbs | 18 ++ .../components/modal/activity-pub-follow.js | 11 + .../discovery-activity-pub-handle.hbs | 2 +- .../activity-pub-category-settings.hbs | 12 +- .../activity-pub-category-navigation.hbs | 0 .../activity-pub-category-navigation.js | 5 + .../activity-pub-category-followers.js | 6 + .../helpers/activity-pub-actor-image.js | 24 ++ .../initializers/activity-pub-initializer.js | 9 + .../discourse/lib/activity-pub-utilities.js | 10 + .../routes/activity-pub-category-followers.js | 25 ++ .../activity-pub-category-followers.hbs | 11 + assets/stylesheets/common/common.scss | 243 +++++++++++++++++- config/locales/client.en.yml | 39 ++- config/locales/server.en.yml | 4 + config/routes.rb | 5 + plugin.rb | 38 ++- .../category_controller_spec.rb | 53 ++++ 40 files changed, 898 insertions(+), 43 deletions(-) create mode 100644 app/controllers/discourse_activity_pub/category_controller.rb create mode 100644 app/serializers/discourse_activity_pub/follower_serializer.rb create mode 100644 assets/javascripts/discourse/activity-pub-category-route-map.js create mode 100644 assets/javascripts/discourse/components/activity-pub-category-banner.hbs create mode 100644 assets/javascripts/discourse/components/activity-pub-category-banner.js create mode 100644 assets/javascripts/discourse/components/activity-pub-follow-btn.hbs create mode 100644 assets/javascripts/discourse/components/activity-pub-follow-btn.js create mode 100644 assets/javascripts/discourse/components/activity-pub-follow-domain.hbs create mode 100644 assets/javascripts/discourse/components/activity-pub-follow-domain.js create mode 100644 assets/javascripts/discourse/components/activity-pub-follower-card.hbs create mode 100644 assets/javascripts/discourse/components/activity-pub-follower-card.js create mode 100644 assets/javascripts/discourse/components/activity-pub-followers-btn.hbs create mode 100644 assets/javascripts/discourse/components/activity-pub-followers-btn.js create mode 100644 assets/javascripts/discourse/components/activity-pub-followers.hbs create mode 100644 assets/javascripts/discourse/components/activity-pub-followers.js create mode 100644 assets/javascripts/discourse/components/activity-pub-statistics.hbs create mode 100644 assets/javascripts/discourse/components/modal/activity-pub-follow.hbs create mode 100644 assets/javascripts/discourse/components/modal/activity-pub-follow.js create mode 100644 assets/javascripts/discourse/connectors/category-navigation/activity-pub-category-navigation.hbs create mode 100644 assets/javascripts/discourse/connectors/category-navigation/activity-pub-category-navigation.js create mode 100644 assets/javascripts/discourse/controllers/activity-pub-category-followers.js create mode 100644 assets/javascripts/discourse/helpers/activity-pub-actor-image.js create mode 100644 assets/javascripts/discourse/lib/activity-pub-utilities.js create mode 100644 assets/javascripts/discourse/routes/activity-pub-category-followers.js create mode 100644 assets/javascripts/discourse/templates/activity-pub-category-followers.hbs create mode 100644 spec/requests/discourse_activity_pub/category_controller_spec.rb diff --git a/app/controllers/discourse_activity_pub/category_controller.rb b/app/controllers/discourse_activity_pub/category_controller.rb new file mode 100644 index 00000000..ec4ce11a --- /dev/null +++ b/app/controllers/discourse_activity_pub/category_controller.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module DiscourseActivityPub + class CategoryController < ApplicationController + PAGE_SIZE = 50 + ORDER = %w(domain username followed_at) + + before_action :ensure_site_enabled + before_action :find_category + + def index + end + + def followers + guardian.ensure_can_see!(@category) + + order_followed_at = params[:order] == 'followed_at' + permitted_order = ORDER.find { |attr| attr == params[:order] } + order = (order_followed_at || !permitted_order) ? 'created_at' : permitted_order + order_dir = params[:asc] ? "ASC" : "DESC" + order_table = order == 'created_at' ? 'discourse_activity_pub_follows' : 'discourse_activity_pub_actors' + + followers = @category + .activity_pub_followers + .joins(:follow_follows) + .where(follow_follows: { followed_id: @category.activity_pub_actor.id }) + .order("#{order_table}.#{order} #{order_dir}") + + limit = fetch_limit_from_params(default: PAGE_SIZE, max: PAGE_SIZE) + page = fetch_int_from_params(:page, default: 0) + total = followers.count + followers = followers.limit(limit).offset(limit * page).to_a + + load_more_params = params.slice(:order, :asc).permit! + load_more_params[:page] = page + 1 + load_more_uri = ::URI.parse("/ap/category/#{params[:category_id]}/followers.json") + load_more_uri.query = ::URI.encode_www_form(load_more_params.to_h) + + serialized = serialize_data(followers, FollowerSerializer, root: false) + render_json_dump( + followers: serialized, + meta: { + total: total, + load_more: load_more_uri.to_s, + } + ) + end + + protected + + def followers_response + + end + + def find_category + @category = Category.find_by_id(params.require(:category_id)) + return render_category_error("category_not_found", 400) unless @category.present? + end + + def ensure_site_enabled + render_category_error("not_enabled", 403) unless DiscourseActivityPub.enabled + end + + def render_category_error(key, status) + render_json_error I18n.t("discourse_activity_pub.category.error.#{key}"), status: status + end + end +end \ No newline at end of file diff --git a/app/models/discourse_activity_pub_actor.rb b/app/models/discourse_activity_pub_actor.rb index 0cfce6a7..fcf310bf 100644 --- a/app/models/discourse_activity_pub_actor.rb +++ b/app/models/discourse_activity_pub_actor.rb @@ -54,8 +54,12 @@ def can_perform_activity?(activity_ap_type, object_ap_type = nil) ) end + def domain + local? ? DiscourseActivityPub.host : self.read_attribute(:domain) + end + def url - local? && model&.activity_pub_url + local? ? model&.activity_pub_url : self.ap_id end def icon_url diff --git a/app/models/discourse_activity_pub_follow.rb b/app/models/discourse_activity_pub_follow.rb index 1a312403..dbeb7944 100644 --- a/app/models/discourse_activity_pub_follow.rb +++ b/app/models/discourse_activity_pub_follow.rb @@ -3,6 +3,10 @@ class DiscourseActivityPubFollow < ActiveRecord::Base belongs_to :follower, class_name: "DiscourseActivityPubActor" belongs_to :followed, class_name: "DiscourseActivityPubActor" + + def followed_at + created_at + end end # == Schema Information diff --git a/app/serializers/discourse_activity_pub/follower_serializer.rb b/app/serializers/discourse_activity_pub/follower_serializer.rb new file mode 100644 index 00000000..2b5b1940 --- /dev/null +++ b/app/serializers/discourse_activity_pub/follower_serializer.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module DiscourseActivityPub + class FollowerSerializer < ActiveModel::Serializer + attributes :username, + :local, + :domain, + :url, + :followed_at, + :icon_url, + :user + + def user + BasicUserSerializer.new(object.model, root: false).as_json + end + + def followed_at + object.follow_follows&.first.created_at + end + end +end diff --git a/assets/javascripts/discourse/activity-pub-category-route-map.js b/assets/javascripts/discourse/activity-pub-category-route-map.js new file mode 100644 index 00000000..b2840fbc --- /dev/null +++ b/assets/javascripts/discourse/activity-pub-category-route-map.js @@ -0,0 +1,5 @@ +export default function() { + this.route("activityPub.category.followers", { + path: "/ap/category/:category_id/followers" + }); +}; \ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-category-banner.hbs b/assets/javascripts/discourse/components/activity-pub-category-banner.hbs new file mode 100644 index 00000000..4d83e77a --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-category-banner.hbs @@ -0,0 +1,8 @@ +
+ {{#if @category}} + + {{#unless this.site.mobileView}} + + {{/unless}} + {{/if}} +
\ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-category-banner.js b/assets/javascripts/discourse/components/activity-pub-category-banner.js new file mode 100644 index 00000000..bad9a0c9 --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-category-banner.js @@ -0,0 +1,6 @@ +import Component from "@glimmer/component"; +import { inject as service } from "@ember/service"; + +export default class ActivityPubCategoryBanner extends Component { + @service site; +} diff --git a/assets/javascripts/discourse/components/activity-pub-discovery.hbs b/assets/javascripts/discourse/components/activity-pub-discovery.hbs index d47111eb..43af1562 100644 --- a/assets/javascripts/discourse/components/activity-pub-discovery.hbs +++ b/assets/javascripts/discourse/components/activity-pub-discovery.hbs @@ -7,7 +7,7 @@ {{#if showDropdown}}
{{i18n "discourse_activity_pub.discovery.description" category_name=@category.name}} - +
{{/if}} \ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-follow-btn.hbs b/assets/javascripts/discourse/components/activity-pub-follow-btn.hbs new file mode 100644 index 00000000..3cf12956 --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-follow-btn.hbs @@ -0,0 +1,8 @@ +
+ +
\ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-follow-btn.js b/assets/javascripts/discourse/components/activity-pub-follow-btn.js new file mode 100644 index 00000000..203784f9 --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-follow-btn.js @@ -0,0 +1,13 @@ +import Component from "@glimmer/component"; +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import ActivityPubFollowModal from "../components/modal/activity-pub-follow"; + +export default class ActivityPubFollowBtn extends Component { + @service modal; + + @action + showModal() { + this.modal.show(ActivityPubFollowModal, { model: this.args.category }); + } +} diff --git a/assets/javascripts/discourse/components/activity-pub-follow-domain.hbs b/assets/javascripts/discourse/components/activity-pub-follow-domain.hbs new file mode 100644 index 00000000..28f47dc6 --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-follow-domain.hbs @@ -0,0 +1,26 @@ +
+ +
+ + +
+
+ {{#if this.error}} + {{this.error}} + {{else if this.verifying}} + {{i18n 'discourse_activity_pub.follow.domain.verifying'}} + {{else}} + {{i18n 'discourse_activity_pub.follow.domain.description'}} + {{/if}} +
+
\ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-follow-domain.js b/assets/javascripts/discourse/components/activity-pub-follow-domain.js new file mode 100644 index 00000000..17d98308 --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-follow-domain.js @@ -0,0 +1,90 @@ +import Component from "@glimmer/component"; +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import { tracked } from "@glimmer/tracking"; +import { ajax } from "discourse/lib/ajax"; +import { hostnameValid, extractDomainFromUrl } from "discourse/lib/utilities"; +import { buildHandle } from "../lib/activity-pub-utilities"; +import I18n from "I18n"; + +// We're using a hardcoded url here as mastodon will only webfinger this as +// an ostatus template if the account is local, which is too limiting. +// See https://docs.joinmastodon.org/spec/webfinger +// See https://socialhub.activitypub.rocks/t/what-is-the-current-spec-for-remote-follow/2020 +const mastodonFollowUrl = (domain, handle) => { + return `https://${domain}/authorize_interaction?uri=${encodeURIComponent(handle)}`; +}; + +// See https://docs.joinmastodon.org/methods/instance/#v2 +const mastodonAboutPath = 'api/v2/instance'; + +export default class ActivityPubFollowMastodon extends Component { + @service site; + @tracked verifying = false; + @tracked error = null; + + get footerClass() { + let result = 'activity-pub-follow-domain-footer'; + if (this.error) { + result += ' error'; + } + return result; + } + + getFollowUrl(domain, handle) { + return new Promise((resolve) => { + if (!hostnameValid(domain)) { + return resolve(null); + } + + return ajax(`https://${domain}/${mastodonAboutPath}`, { + type: 'GET', + ignoreUnsent: false + }).then(response => { + if (response?.domain && response.domain === domain) { + return resolve(mastodonFollowUrl(domain, handle)); + } else { + return resolve(null); + } + }).catch(error => { + return resolve(null); + }) + }); + } + + @action + onKeyup(e) { + if (e.key === "Enter") { + this.follow(); + } + } + + @action + async follow() { + if (!this.domain) { + return; + } + + const model = this.args.model; + const site = this.site; + const handle = buildHandle({ model, site }); + + if (!handle) { + return; + } + + this.error = null; + this.verifying = true; + + const domain = extractDomainFromUrl(this.domain); + const url = await this.getFollowUrl(domain, handle); + + this.verifying = false; + + if (url) { + window.open(url, '_blank').focus(); + } else { + this.error = I18n.t("discourse_activity_pub.follow.domain.invalid"); + } + } +} diff --git a/assets/javascripts/discourse/components/activity-pub-follower-card.hbs b/assets/javascripts/discourse/components/activity-pub-follower-card.hbs new file mode 100644 index 00000000..5ea4e7d4 --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-follower-card.hbs @@ -0,0 +1,32 @@ +
+
+
+ {{activityPubActorImage @follower size=imageSize classes=imageClasses}} +
+
+ +
+ {{#if @follower.user}} + + {{/if}} +
+
+
+
+ +
{{i18n "discourse_activity_pub.follower.followed_at"}}
+
{{bound-date @follower.followed_at}}
+
+
+
+
+
\ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-follower-card.js b/assets/javascripts/discourse/components/activity-pub-follower-card.js new file mode 100644 index 00000000..825b4a82 --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-follower-card.js @@ -0,0 +1,21 @@ +import Component from "@glimmer/component"; +import { inject as service } from "@ember/service"; + +export default class ActivityPubFollowerCard extends Component { + @service siteSettings; + @service site; + + get imageSize() { + return this.site.mobileView ? 'small' : 'huge'; + } + + get classes() { + let result = 'activity-pub-follower-card'; + if (this.site.mobileView) { + result += ' mobile'; + } else { + result += ' desktop'; + } + return result; + } +} diff --git a/assets/javascripts/discourse/components/activity-pub-followers-btn.hbs b/assets/javascripts/discourse/components/activity-pub-followers-btn.hbs new file mode 100644 index 00000000..589f6dce --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-followers-btn.hbs @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-followers-btn.js b/assets/javascripts/discourse/components/activity-pub-followers-btn.js new file mode 100644 index 00000000..362157cf --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-followers-btn.js @@ -0,0 +1,33 @@ +import Component from "@glimmer/component"; +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import I18n from "I18n"; + +export default class ActivityPubFollowersBtn extends Component { + @service router; + + get label() { + return I18n.t('discourse_activity_pub.followers.label', { + count: this.args.category.activity_pub_follower_count + }) + } + + get title() { + return I18n.t('discourse_activity_pub.followers.title', { + count: this.args.category.activity_pub_follower_count + }) + } + + get classes() { + let result = "activity-pub-followers-btn"; + if (this.router.currentRouteName === 'activityPub.category.followers') { + result += " active"; + } + return result; + } + + @action + goToFollowers() { + this.router.transitionTo(`/ap/category/${this.args.category.id}/followers`); + } +}; diff --git a/assets/javascripts/discourse/components/activity-pub-followers.hbs b/assets/javascripts/discourse/components/activity-pub-followers.hbs new file mode 100644 index 00000000..c91f7825 --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-followers.hbs @@ -0,0 +1,5 @@ +
+ {{#each @followers as |follower|}} + + {{/each}} +
\ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-followers.js b/assets/javascripts/discourse/components/activity-pub-followers.js new file mode 100644 index 00000000..ed53e513 --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-followers.js @@ -0,0 +1,3 @@ +import Component from "@glimmer/component"; + +export default class ActivityPubFollowers extends Component {} diff --git a/assets/javascripts/discourse/components/activity-pub-handle.hbs b/assets/javascripts/discourse/components/activity-pub-handle.hbs index 54f28848..064f3b75 100644 --- a/assets/javascripts/discourse/components/activity-pub-handle.hbs +++ b/assets/javascripts/discourse/components/activity-pub-handle.hbs @@ -1,16 +1,26 @@
- {{handle}} - {{#if this.copied}} - - {{else}} - - {{/if}} +
+ {{handle}} + {{#if @actor.url}} + {{d-icon "external-link-alt"}} + {{/if}} + {{#if this.copied}} + + {{else}} + + {{/if}} +
\ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-handle.js b/assets/javascripts/discourse/components/activity-pub-handle.js index d2f85695..3493d007 100644 --- a/assets/javascripts/discourse/components/activity-pub-handle.js +++ b/assets/javascripts/discourse/components/activity-pub-handle.js @@ -4,17 +4,18 @@ import { inject as service } from "@ember/service"; import discourseLater from "discourse-common/lib/later"; import { tracked } from "@glimmer/tracking"; import { clipboardCopy } from "discourse/lib/utilities"; +import { buildHandle } from "../lib/activity-pub-utilities"; export default class ActivityPubHandle extends Component { @tracked copied = false; @service site; + @service siteSettings; get handle() { - if (!this.args.actor) { - return undefined; - } else { - return `${this.args.actor.activity_pub_username}@${this.site.activity_pub_host}`; - } + const model = this.args.model; + const actor = this.args.actor; + const site = this.site; + return buildHandle({ actor, model, site }); } @action diff --git a/assets/javascripts/discourse/components/activity-pub-statistics.hbs b/assets/javascripts/discourse/components/activity-pub-statistics.hbs new file mode 100644 index 00000000..c378dd77 --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-statistics.hbs @@ -0,0 +1,8 @@ +
+ + {{i18n (concat "discourse_activity_pub.visibility.label." @category.activity_pub_default_visibility)}} + + + {{i18n (concat "discourse_activity_pub.publication_type.label." @category.activity_pub_publication_type)}} + +
\ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-status.hbs b/assets/javascripts/discourse/components/activity-pub-status.hbs index b2826281..0f64ee04 100644 --- a/assets/javascripts/discourse/components/activity-pub-status.hbs +++ b/assets/javascripts/discourse/components/activity-pub-status.hbs @@ -2,3 +2,15 @@ {{d-icon "discourse-activity-pub"}} {{this.translatedLabel}} + +{{#if @showTip}} +
+ +
+
+ {{i18n "discourse_activity_pub.tip.status.description"}} +
+
+
+
+{{/if}} diff --git a/assets/javascripts/discourse/components/modal/activity-pub-follow.hbs b/assets/javascripts/discourse/components/modal/activity-pub-follow.hbs new file mode 100644 index 00000000..ffa3a1fd --- /dev/null +++ b/assets/javascripts/discourse/components/modal/activity-pub-follow.hbs @@ -0,0 +1,18 @@ + + <:body> +
+ + + +
+ {{i18n "discourse_activity_pub.handle.description"}} +
+
+ +
\ No newline at end of file diff --git a/assets/javascripts/discourse/components/modal/activity-pub-follow.js b/assets/javascripts/discourse/components/modal/activity-pub-follow.js new file mode 100644 index 00000000..9f8db709 --- /dev/null +++ b/assets/javascripts/discourse/components/modal/activity-pub-follow.js @@ -0,0 +1,11 @@ +import Component from "@glimmer/component"; +import I18n from "I18n"; + +export default class ActivityPubFollow extends Component { + get title() { + return I18n.t("discourse_activity_pub.follow.title", { + name: this.args.model.name + }); + } + +} diff --git a/assets/javascripts/discourse/connectors/before-create-topic-button/discovery-activity-pub-handle.hbs b/assets/javascripts/discourse/connectors/before-create-topic-button/discovery-activity-pub-handle.hbs index 35dad939..20412146 100644 --- a/assets/javascripts/discourse/connectors/before-create-topic-button/discovery-activity-pub-handle.hbs +++ b/assets/javascripts/discourse/connectors/before-create-topic-button/discovery-activity-pub-handle.hbs @@ -1,3 +1,3 @@ {{#if this.category.activity_pub_show_handle}} - + {{/if}} \ No newline at end of file diff --git a/assets/javascripts/discourse/connectors/category-custom-settings/activity-pub-category-settings.hbs b/assets/javascripts/discourse/connectors/category-custom-settings/activity-pub-category-settings.hbs index 51103002..499b754a 100644 --- a/assets/javascripts/discourse/connectors/category-custom-settings/activity-pub-category-settings.hbs +++ b/assets/javascripts/discourse/connectors/category-custom-settings/activity-pub-category-settings.hbs @@ -33,10 +33,20 @@ +
+ +
+ {{i18n 'category.discourse_activity_pub.show_banner_description'}} +
+
+ {{#if this.category.activity_pub_username}}
- +
{{i18n 'category.discourse_activity_pub.handle_description'}}
diff --git a/assets/javascripts/discourse/connectors/category-navigation/activity-pub-category-navigation.hbs b/assets/javascripts/discourse/connectors/category-navigation/activity-pub-category-navigation.hbs new file mode 100644 index 00000000..e69de29b diff --git a/assets/javascripts/discourse/connectors/category-navigation/activity-pub-category-navigation.js b/assets/javascripts/discourse/connectors/category-navigation/activity-pub-category-navigation.js new file mode 100644 index 00000000..a45ffb36 --- /dev/null +++ b/assets/javascripts/discourse/connectors/category-navigation/activity-pub-category-navigation.js @@ -0,0 +1,5 @@ +export default { + shouldRender(_, ctx) { + return ctx.site.activity_pub_enabled; + }, +}; diff --git a/assets/javascripts/discourse/controllers/activity-pub-category-followers.js b/assets/javascripts/discourse/controllers/activity-pub-category-followers.js new file mode 100644 index 00000000..1b2c72b5 --- /dev/null +++ b/assets/javascripts/discourse/controllers/activity-pub-category-followers.js @@ -0,0 +1,6 @@ +import Controller from "@ember/controller"; +import { inject as service } from "@ember/service"; + +export default class ActivityPubCategoryFollowers extends Controller { + @service composer; +} diff --git a/assets/javascripts/discourse/helpers/activity-pub-actor-image.js b/assets/javascripts/discourse/helpers/activity-pub-actor-image.js new file mode 100644 index 00000000..673c5aca --- /dev/null +++ b/assets/javascripts/discourse/helpers/activity-pub-actor-image.js @@ -0,0 +1,24 @@ +import { htmlSafe } from "@ember/template"; +import { translateSize, avatarUrl } from "discourse-common/lib/avatar-utils"; +import { registerRawHelper } from "discourse-common/lib/helpers"; +import { buildHandle } from "../lib/activity-pub-utilities"; + +function renderActivityPubActorImage(actor, opts) { + opts = opts || {}; + + if (actor) { + const size = translateSize(opts.size); + const url = actor.icon_url || "/images/avatar.png"; + const title = buildHandle({ actor }); + const img = ``; + return `
${img}
`; + } else { + return ""; + } +} + +registerRawHelper("activityPubActorImage", activityPubActorImage); + +export default function activityPubActorImage(actor, params) { + return htmlSafe(renderActivityPubActorImage.call(this, actor, params)); +} diff --git a/assets/javascripts/discourse/initializers/activity-pub-initializer.js b/assets/javascripts/discourse/initializers/activity-pub-initializer.js index 9bef9f1f..de64f62a 100644 --- a/assets/javascripts/discourse/initializers/activity-pub-initializer.js +++ b/assets/javascripts/discourse/initializers/activity-pub-initializer.js @@ -250,6 +250,15 @@ export default { ); }, }); + + api.addNavigationBarItem({ + name: "followers", + title: I18n.t('discourse_activity_pub.followers.description'), + displayName: I18n.t('discourse_activity_pub.followers.label'), + customFilter: (category, args, router) => (category?.activity_pub_enabled), + customHref: (category, args, router) => (`/ap/category/${category.id}/followers`), + forceActive: (category, args, router) => router.currentURL === `/ap/category/${category.id}/followers` + }) }); }, }; diff --git a/assets/javascripts/discourse/lib/activity-pub-utilities.js b/assets/javascripts/discourse/lib/activity-pub-utilities.js new file mode 100644 index 00000000..397dc133 --- /dev/null +++ b/assets/javascripts/discourse/lib/activity-pub-utilities.js @@ -0,0 +1,10 @@ + +export function buildHandle({ actor, model, site }) { + if ((!actor && !model) || (model && !site)) { + return undefined; + } else { + const username = actor ? actor.username : model.activity_pub_username; + const domain = actor ? actor.domain : site.activity_pub_host; + return `${username}@${domain}`; + } +} \ No newline at end of file diff --git a/assets/javascripts/discourse/routes/activity-pub-category-followers.js b/assets/javascripts/discourse/routes/activity-pub-category-followers.js new file mode 100644 index 00000000..e0db293a --- /dev/null +++ b/assets/javascripts/discourse/routes/activity-pub-category-followers.js @@ -0,0 +1,25 @@ +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import DiscourseRoute from "discourse/routes/discourse"; +import Category from "discourse/models/category"; + +export default DiscourseRoute.extend({ + queryParams: { + order: { refreshModel: true }, + asc: { refreshModel: true }, + domain: { refreshModel: true }, + username: { refreshModel: true } + }, + + model(params) { + const category = Category.findById(params.category_id); + + return ajax(`/ap/category/${category.id}/followers.json`) + .then(response => ({ category, ...response })) + .catch(popupAjaxError); + }, + + setupController(controller, model) { + controller.setProperties(model); + }, +}); diff --git a/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs b/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs new file mode 100644 index 00000000..9137af9d --- /dev/null +++ b/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs @@ -0,0 +1,11 @@ + +{{#if this.category.activity_pub_show_banner}} + +{{/if}} + \ No newline at end of file diff --git a/assets/stylesheets/common/common.scss b/assets/stylesheets/common/common.scss index 73d02a74..4ae3c235 100644 --- a/assets/stylesheets/common/common.scss +++ b/assets/stylesheets/common/common.scss @@ -1,3 +1,13 @@ +@import "common/components/buttons"; + +@mixin description-text { + color: var(--primary-medium); + margin-top: 4px; + margin-bottom: 10px; + font-size: var(--font-down-1); + line-height: var(--line-height-large); +} + .activity-pub-category-settings-title { display: flex; align-items: center; @@ -33,6 +43,18 @@ } } +.activity-pub-status-tip { + background: var(--primary-low); + height: 35px; + padding: 0 8px 0 2px; + align-items: center; + display: flex; + + .d-icon { + color: var(--primary-medium); + } +} + .activity-pub-setting { margin-top: 4px; @@ -41,11 +63,7 @@ } .activity-pub-setting-description { - color: var(--primary-medium); - margin-top: 4px; - margin-bottom: 10px; - font-size: var(--font-down-1); - line-height: var(--line-height-large); + @include description-text; } .activity-pub-setting-notice { @@ -73,22 +91,35 @@ } .activity-pub-handle { - display: inline-flex; - align-items: stretch; + overflow: hidden; - .handle { + .activity-pub-handle-contents { + display: flex; + align-items: stretch; border-radius: var(--d-border-radius); - background-color: var(--primary-200); + border: 1px solid var(--primary-low); + line-height: 30px; + overflow: hidden; + } + + .handle { padding: 0 0.65em; - line-height: 35px; + color: var(--primary-high); + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; } - button { - border: unset; - max-height: 35px; + button, + a { + @extend .btn-flat; } } +.activity-pub-handle-description { + @include description-text; +} + .activity-pub-discovery-dropdown { display: flex; flex-direction: column; @@ -185,3 +216,189 @@ body.user-preferences-activity-pub-page { align-items: center; } } + +.activity-pub-category-navigation { + width: 100%; +} + +.activity-pub-category-banner { + display: flex; + align-items: center; + width: 100%; + background: var(--primary-very-low); + + .activity-pub-status { + padding: 6px 8px; + } + + .activity-pub-statistics { + flex: 1; + display: inline-flex; + align-items: center; + gap: 1.5em; + height: 100%; + padding: 0 1.5em; + + @media (max-width: 700px) { + display: none; + } + } + + .activity-pub-follow-btn { + margin-left: auto; + } + + button { + min-height: 35px; + } +} + +.activity-pub-followers-btn { + &.active { + background-color: var(--quaternary); + color: var(--secondary); + } +} + +.activity-pub-followers { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); + grid-gap: 60px 20px; + margin-top: 60px; + padding: 1em 0; +} + +.activity-pub-follower { + background: var(--secondary); + box-shadow: var(--shadow-card); +} + +.activity-pub-follower-content { + padding: 1em; +} + +.activity-pub-follower-user { + display: flex; + align-items: center; + + .activity-pub-site-icon { + margin-right: 0.5em; + + img { + width: 24px; + height: 24px; + } + } +} + +.activity-pub-follower-card { + --avatar-width-desktop: 8em; + --avatar-margin-desktop: -3.3em; + --avatar-width-mobile: 4em; + --avatar-margin-mobile: 0; + + box-shadow: var(--shadow-card); + padding: 1em; + display: flex; + max-width: calc(100vw - 20px); + box-sizing: border-box; + + &.desktop { + .activity-pub-actor-image { + margin-top: var(--avatar-margin-desktop); + max-height: var(--avatar-width-desktop); + + img { + width: var(--avatar-width-desktop); + height: var(--avatar-width-desktop); + } + } + } + + &.mobile { + .activity-pub-actor-image { + margin-top: var(--avatar-margin-mobile); + max-height: var(--avatar-width-mobile); + + img { + width: var(--avatar-width-mobile); + height: var(--avatar-width-mobile); + } + } + } +} + +.activity-pub-follower-card-content { + display: flex; + flex-direction: column; + gap: 1em; + max-width: 100%; + + .row { + display: flex; + gap: 1em; + max-width: 100%; + } + + .top-content { + display: flex; + flex-direction: column; + gap: 0.75em; + overflow: hidden; + } +} + +.activity-pub-actor-image { + img { + border-radius: 50%; + } +} + +.activity-pub-actor-statistics { + display: flex; + gap: 1em; + + dt, + dd { + display: inline-flex; + align-items: center; + } + + dd { + padding: 0; + margin: 0 15px 0 0; + color: var(--primary); + } + + dt { + color: var(--secondary-medium); + margin-right: 5px; + display: inline-block; + } +} + +.activity-pub-follow-controls { + .activity-pub-handle-label { + margin-top: 1em; + } + .activity-pub-handle { + display: inline-flex; + } +} + +.activity-pub-follow-domain-controls { + display: flex; + align-items: center; + + input { + margin: 0; + } +} + +.activity-pub-follow-domain-footer { + @include description-text; + + &.error { + color: var(--danger); + } +} \ No newline at end of file diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 83e9158f..be7670d9 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -7,8 +7,10 @@ en: enable_description: Site settings 'activity pub enabled' and 'login required' also affect whether ActivityPub is active. show_status: Show the ActivityPub status and visibility in this category's composer. show_status_description: Shows in the composer when creating a post that will be published with ActivityPub. - show_handle: Show an ActivityPub handle in this category's topic list. - show_handle_description: Shows a dropdown with the handle to the left of the "New Topic" button. + show_handle: Show ActivityPub follow button in this category's topic list. + show_handle_description: Shows a follow button to the left of the "New Topic" button. + show_banner: Show an ActivityPub banner in this category's topic list. + show_banner_description: Shows a banner with ActivityPub statistics above the topic list in this category. username: ActivityPub username for this category. username_description: Cannot be changed once saved. handle: ActivityPub handle for this category. @@ -37,13 +39,38 @@ en: not_active: Not Active discovery: description: Follow %{category_name} on services that support ActivityPub using + handle: + label: Other + description: Search for this handle on a service that supports ActivityPub. + follow: + label: Follow + title: Follow %{name} via ActivityPub + domain: + label: Mastodon + placeholder: domain + btn_label: Follow + btn_title: Follow on this Mastodon domain + description: Domain of your Mastodon account. + verifying: Verifying the domain... + invalid: Not a valid Mastodon domain. + followers: + label: Followers + description: People following this category via ActivityPub. + label_with_count: + one: "%{count} Follower" + other: "%{count} Followers" + title: + one: "%{count} person is following this category via ActivityPub." + other: "%{count} people are following this category via ActivityPub." + follower: + followed_at: Followed visibility: label: private: Followers Only public: Public description: - private: "%{object_type} is addressed to followers." - public: "%{object_type} is publicly addressed." + private: "%{object_type}s are addressed to followers." + public: "%{object_type}s are publicly addressed." post_object_type: label: note: Note @@ -58,6 +85,10 @@ en: description: first_post: First post of every topic in this category is published via ActivityPub. full_topic: Every post of every topic in this category is published via ActivityPub and replies on other ActivityPub servers are imported as replies in Discourse. + tip: + status: + description: You can follow this category on any social network that supports ActivityPub, such as Mastodon. + post: discourse_activity_pub: title: diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 1164a621..b6ac9644 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -92,6 +92,10 @@ en: not_enabled: "Webfinger is not enabled" resource_not_supported: "Resource not supported" resource_not_found: "Resource not found" + category: + error: + not_enabled: "ActivityPub is not enabled" + category_not_found: "Category not found" post: error: not_enabled: "ActivityPub is not enabled" diff --git a/config/routes.rb b/config/routes.rb index 352129b3..95e983dc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -6,6 +6,11 @@ delete "schedule/:post_id" => "post#unschedule" end + scope '/category' do + get "/:category_id" => "category#index" + get "/:category_id/followers" => "category#followers" + end + scope module: 'a_p' do get "actor/:key" => "actors#show" post "actor/:key/inbox" => "inboxes#create" diff --git a/plugin.rb b/plugin.rb index de73e892..07015816 100644 --- a/plugin.rb +++ b/plugin.rb @@ -75,6 +75,7 @@ ../app/controllers/discourse_activity_pub/auth/oauth_controller.rb ../app/controllers/discourse_activity_pub/auth/authorization_controller.rb ../app/controllers/discourse_activity_pub/post_controller.rb + ../app/controllers/discourse_activity_pub/category_controller.rb ../app/serializers/discourse_activity_pub/ap/object_serializer.rb ../app/serializers/discourse_activity_pub/ap/activity_serializer.rb ../app/serializers/discourse_activity_pub/ap/activity/response_serializer.rb @@ -96,6 +97,7 @@ ../app/serializers/discourse_activity_pub/ap/collection_serializer.rb ../app/serializers/discourse_activity_pub/ap/collection/ordered_collection_serializer.rb ../app/serializers/discourse_activity_pub/webfinger_serializer.rb + ../app/serializers/discourse_activity_pub/follower_serializer.rb ../app/serializers/discourse_activity_pub/auth/authorization_serializer.rb ../config/routes.rb ../extensions/discourse_activity_pub_category_extension.rb @@ -111,11 +113,16 @@ class_name: "DiscourseActivityPubActor", as: :model, dependent: :destroy + Category.has_many :activity_pub_followers, + through: :activity_pub_actor, + source: :followers, + class_name: "DiscourseActivityPubActor" Category.prepend DiscourseActivityPubCategoryExtension register_category_custom_field_type("activity_pub_enabled", :boolean) register_category_custom_field_type("activity_pub_show_status", :boolean) register_category_custom_field_type("activity_pub_show_handle", :boolean) + register_category_custom_field_type("activity_pub_show_banner", :boolean) register_category_custom_field_type("activity_pub_username", :string) register_category_custom_field_type("activity_pub_name", :string) register_category_custom_field_type("activity_pub_default_visibility", :string) @@ -138,6 +145,9 @@ add_to_class(:category, :activity_pub_show_handle) do DiscourseActivityPub.enabled && !!custom_fields["activity_pub_show_handle"] end + add_to_class(:category, :activity_pub_show_banner) do + DiscourseActivityPub.enabled && !!custom_fields["activity_pub_show_banner"] + end add_to_class(:category, :activity_pub_ready?) do activity_pub_enabled && activity_pub_actor.present? && activity_pub_actor.persisted? @@ -186,12 +196,12 @@ add_to_class(:category, :activity_pub_full_topic) do activity_pub_publication_type === 'full_topic' end - add_to_class(:category, :activity_pub_post_object_type) do - custom_fields["activity_pub_post_object_type"] - end add_to_class(:category, :activity_pub_default_object_type) do DiscourseActivityPub::AP::Actor::Group.type end + add_to_class(:category, :activity_pub_follower_count) do + activity_pub_followers.count + end add_model_callback(:category, :after_save) do DiscourseActivityPubActor.ensure_for(self) @@ -239,20 +249,42 @@ :activity_pub_show_handle, include_condition: -> { object.activity_pub_enabled } ) { object.activity_pub_show_handle } + add_to_serializer( + :basic_category, + :activity_pub_show_banner, + include_condition: -> { object.activity_pub_enabled } + ) { object.activity_pub_show_banner } add_to_serializer( :basic_category, :activity_pub_default_visibility, include_condition: -> { object.activity_pub_enabled } ) { object.activity_pub_default_visibility } + add_to_serializer( + :basic_category, + :activity_pub_follower_count, + include_condition: -> { object.activity_pub_enabled } + ) { object.activity_pub_follower_count } + add_to_serializer( + :basic_category, + :activity_pub_post_object_type, + include_condition: -> { object.activity_pub_post_object_type } + ) { object.activity_pub_post_object_type } + add_to_serializer( + :basic_category, + :activity_pub_publication_type, + include_condition: -> { object.activity_pub_publication_type } + ) { object.activity_pub_publication_type } if Site.respond_to? :preloaded_category_custom_fields Site.preloaded_category_custom_fields << "activity_pub_enabled" Site.preloaded_category_custom_fields << "activity_pub_ready" Site.preloaded_category_custom_fields << "activity_pub_show_status" Site.preloaded_category_custom_fields << "activity_pub_show_handle" + Site.preloaded_category_custom_fields << "activity_pub_show_banner" Site.preloaded_category_custom_fields << "activity_pub_username" Site.preloaded_category_custom_fields << "activity_pub_name" Site.preloaded_category_custom_fields << "activity_pub_default_visibility" + Site.preloaded_category_custom_fields << "activity_pub_post_object_type" Site.preloaded_category_custom_fields << "activity_pub_publication_type" end diff --git a/spec/requests/discourse_activity_pub/category_controller_spec.rb b/spec/requests/discourse_activity_pub/category_controller_spec.rb new file mode 100644 index 00000000..75f79cb0 --- /dev/null +++ b/spec/requests/discourse_activity_pub/category_controller_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +RSpec.describe DiscourseActivityPub::CategoryController do + let!(:actor) { Fabricate(:discourse_activity_pub_actor_group) } + let!(:follower1) { Fabricate(:discourse_activity_pub_actor_person, domain: 'google.com', username: 'bob') } + let!(:follow1) { Fabricate(:discourse_activity_pub_follow, follower: follower1, followed: actor, created_at: (DateTime.now - 2)) } + let!(:follower2) { Fabricate(:discourse_activity_pub_actor_person, domain: 'twitter.com', username: 'jenny') } + let!(:follow2) { Fabricate(:discourse_activity_pub_follow, follower: follower2, followed: actor, created_at: (DateTime.now - 1)) } + let!(:follower3) { Fabricate(:discourse_activity_pub_actor_person, domain: 'netflix.com', username: 'xavier') } + let!(:follow3) { Fabricate(:discourse_activity_pub_follow, follower: follower3, followed: actor, created_at: DateTime.now) } + + def build_error(key) + { "errors" => [I18n.t("discourse_activity_pub.auth.error.#{key}")] } + end + + describe "#followers" do + before do + toggle_activity_pub(actor.model) + end + + it "returns the categories followers" do + get "/c/#{actor.model.slug}/#{actor.model.id}/ap/followers.json" + expect(response.status).to eq(200) + expect(response.parsed_body['followers'].map{|f| f["ap_id"] }).to eq( + [follower3.ap_id, follower2.ap_id, follower1.ap_id] + ) + end + + it "orders by ap domain" do + get "/c/#{actor.model.slug}/#{actor.model.id}/ap/followers.json?order=domain" + expect(response.status).to eq(200) + expect(response.parsed_body['followers'].map{|f| f["domain"] }).to eq( + ["twitter.com", "netflix.com", "google.com"] + ) + end + + it "orders by ap username" do + get "/c/#{actor.model.slug}/#{actor.model.id}/ap/followers.json?order=username" + expect(response.status).to eq(200) + expect(response.parsed_body['followers'].map{|f| f["username"] }).to eq( + ["xavier", "jenny", "bob"] + ) + end + + it "paginates" do + get "/c/#{actor.model.slug}/#{actor.model.id}/ap/followers.json?limit=2&page=1" + expect(response.status).to eq(200) + expect(response.parsed_body['followers'].map{|f| f["ap_id"] }).to eq( + [follower1.ap_id] + ) + end + end +end \ No newline at end of file From 6d3d8a7e30cfd46795905f0daaa542bad8c3eab4 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Mon, 30 Oct 2023 14:51:39 +0800 Subject: [PATCH 02/71] move status label to follow btn --- .../components/activity-pub-follow-btn.hbs | 12 +++--- .../components/activity-pub-followers-btn.hbs | 6 --- .../components/activity-pub-followers-btn.js | 33 -------------- .../components/activity-pub-status.hbs | 16 +------ .../components/activity-pub-status.js | 26 ++++++++++- .../activity-pub-category-followers.hbs | 3 -- assets/stylesheets/common/common.scss | 43 +++++++++++++------ config/locales/client.en.yml | 8 ++-- 8 files changed, 67 insertions(+), 80 deletions(-) delete mode 100644 assets/javascripts/discourse/components/activity-pub-followers-btn.hbs delete mode 100644 assets/javascripts/discourse/components/activity-pub-followers-btn.js diff --git a/assets/javascripts/discourse/components/activity-pub-follow-btn.hbs b/assets/javascripts/discourse/components/activity-pub-follow-btn.hbs index 3cf12956..5a1419ec 100644 --- a/assets/javascripts/discourse/components/activity-pub-follow-btn.hbs +++ b/assets/javascripts/discourse/components/activity-pub-follow-btn.hbs @@ -1,8 +1,10 @@
-
\ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-followers-btn.hbs b/assets/javascripts/discourse/components/activity-pub-followers-btn.hbs deleted file mode 100644 index 589f6dce..00000000 --- a/assets/javascripts/discourse/components/activity-pub-followers-btn.hbs +++ /dev/null @@ -1,6 +0,0 @@ - \ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-followers-btn.js b/assets/javascripts/discourse/components/activity-pub-followers-btn.js deleted file mode 100644 index 362157cf..00000000 --- a/assets/javascripts/discourse/components/activity-pub-followers-btn.js +++ /dev/null @@ -1,33 +0,0 @@ -import Component from "@glimmer/component"; -import { action } from "@ember/object"; -import { inject as service } from "@ember/service"; -import I18n from "I18n"; - -export default class ActivityPubFollowersBtn extends Component { - @service router; - - get label() { - return I18n.t('discourse_activity_pub.followers.label', { - count: this.args.category.activity_pub_follower_count - }) - } - - get title() { - return I18n.t('discourse_activity_pub.followers.title', { - count: this.args.category.activity_pub_follower_count - }) - } - - get classes() { - let result = "activity-pub-followers-btn"; - if (this.router.currentRouteName === 'activityPub.category.followers') { - result += " active"; - } - return result; - } - - @action - goToFollowers() { - this.router.transitionTo(`/ap/category/${this.args.category.id}/followers`); - } -}; diff --git a/assets/javascripts/discourse/components/activity-pub-status.hbs b/assets/javascripts/discourse/components/activity-pub-status.hbs index 0f64ee04..201c03db 100644 --- a/assets/javascripts/discourse/components/activity-pub-status.hbs +++ b/assets/javascripts/discourse/components/activity-pub-status.hbs @@ -1,16 +1,4 @@ -
+
{{d-icon "discourse-activity-pub"}} - {{this.translatedLabel}} + {{this.translatedLabel}}
- -{{#if @showTip}} -
- -
-
- {{i18n "discourse_activity_pub.tip.status.description"}} -
-
-
-
-{{/if}} diff --git a/assets/javascripts/discourse/components/activity-pub-status.js b/assets/javascripts/discourse/components/activity-pub-status.js index 1b1e098e..56749295 100644 --- a/assets/javascripts/discourse/components/activity-pub-status.js +++ b/assets/javascripts/discourse/components/activity-pub-status.js @@ -1,6 +1,7 @@ import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { inject as service } from "@ember/service"; +import { action } from "@ember/object"; import { bind } from "discourse-common/utils/decorators"; import I18n from "I18n"; @@ -56,6 +57,10 @@ export default class ActivityPubStatus extends Component { } get translatedTitle() { + if (this.args.translatedTitle) { + return this.args.translatedTitle; + } + const args = { model_type: this.args.modelType, }; @@ -96,6 +101,14 @@ export default class ActivityPubStatus extends Component { return this.active ? "active" : "not_active"; } + get classes() { + let result = `activity-pub-status ${this.statusClass}`; + if (this.args.onClick) { + result += ' clickable'; + } + return result; + } + get statusClass() { return this.active ? "active" : "not-active"; } @@ -109,6 +122,17 @@ export default class ActivityPubStatus extends Component { } get translatedLabel() { - return I18n.t(this.labelKey("label")); + if (this.args.translatedLabel) { + return this.args.translatedLabel; + } else { + return I18n.t(this.labelKey("label")); + } + } + + @action + click(event) { + if (this.args.onClick) { + this.args.onClick(event); + } } } diff --git a/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs b/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs index 9137af9d..2310e104 100644 --- a/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs +++ b/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs @@ -5,7 +5,4 @@ }} @category={{this.category}} /> -{{#if this.category.activity_pub_show_banner}} - -{{/if}} \ No newline at end of file diff --git a/assets/stylesheets/common/common.scss b/assets/stylesheets/common/common.scss index 4ae3c235..83cead04 100644 --- a/assets/stylesheets/common/common.scss +++ b/assets/stylesheets/common/common.scss @@ -18,40 +18,44 @@ } } -.activity-pub-status { +div.activity-pub-status { padding: 2px 8px; border-radius: var(--d-border-radius); - font-size: 0.9em; line-height: 0.9em; min-height: 23px; background-color: var(--primary-200); display: inline-flex; align-items: center; - .svg-icon { - height: 1.5em; - width: 1.5em; - color: var(--danger) !important; + > .svg-icon { + height: 1.2em; + width: 1.2em; + color: var(--danger); } - &.active .svg-icon { - color: var(--success) !important; + &.active > .svg-icon { + color: var(--success); } .label { margin-left: 0.5em; } + + .clickable { + cursor: pointer; + } } .activity-pub-status-tip { background: var(--primary-low); - height: 35px; - padding: 0 8px 0 2px; align-items: center; display: flex; + min-height: 30px; + margin-left: 0.5em; .d-icon { color: var(--primary-medium); + font-size: 1rem; } } @@ -139,6 +143,14 @@ #reply-control .activity-pub-status { margin-left: 1em; + + > .svg-icon { + color: var(--danger) !important; + } + + &.active > .svg-icon { + color: var(--success) !important; + } } .post-info.activity-pub { @@ -253,10 +265,13 @@ body.user-preferences-activity-pub-page { } } -.activity-pub-followers-btn { - &.active { - background-color: var(--quaternary); - color: var(--secondary); +.activity-pub-follow-btn { + .activity-pub-status { + @include btn; + + .label { + margin-left: unset; + } } } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index be7670d9..d10ba2e1 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -41,7 +41,7 @@ en: description: Follow %{category_name} on services that support ActivityPub using handle: label: Other - description: Search for this handle on a service that supports ActivityPub. + description: search for this handle on a service that supports ActivityPub follow: label: Follow title: Follow %{name} via ActivityPub @@ -50,9 +50,9 @@ en: placeholder: domain btn_label: Follow btn_title: Follow on this Mastodon domain - description: Domain of your Mastodon account. - verifying: Verifying the domain... - invalid: Not a valid Mastodon domain. + description: domain of your Mastodon account + verifying: verifying the domain... + invalid: not a valid Mastodon domain followers: label: Followers description: People following this category via ActivityPub. From 94b8a0356c0f604dfdf2459bb12ae74db06281d2 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Mon, 30 Oct 2023 14:55:48 +0800 Subject: [PATCH 03/71] cleanup --- .../discourse_activity_pub/category_controller.rb | 4 ---- .../activity-pub-category-settings.hbs | 10 ---------- config/locales/client.en.yml | 2 -- plugin.rb | 11 ----------- 4 files changed, 27 deletions(-) diff --git a/app/controllers/discourse_activity_pub/category_controller.rb b/app/controllers/discourse_activity_pub/category_controller.rb index ec4ce11a..c571b4e7 100644 --- a/app/controllers/discourse_activity_pub/category_controller.rb +++ b/app/controllers/discourse_activity_pub/category_controller.rb @@ -48,10 +48,6 @@ def followers protected - def followers_response - - end - def find_category @category = Category.find_by_id(params.require(:category_id)) return render_category_error("category_not_found", 400) unless @category.present? diff --git a/assets/javascripts/discourse/connectors/category-custom-settings/activity-pub-category-settings.hbs b/assets/javascripts/discourse/connectors/category-custom-settings/activity-pub-category-settings.hbs index 499b754a..30b8b39d 100644 --- a/assets/javascripts/discourse/connectors/category-custom-settings/activity-pub-category-settings.hbs +++ b/assets/javascripts/discourse/connectors/category-custom-settings/activity-pub-category-settings.hbs @@ -33,16 +33,6 @@
-
- -
- {{i18n 'category.discourse_activity_pub.show_banner_description'}} -
-
- {{#if this.category.activity_pub_username}}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index d10ba2e1..8ba320f1 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -9,8 +9,6 @@ en: show_status_description: Shows in the composer when creating a post that will be published with ActivityPub. show_handle: Show ActivityPub follow button in this category's topic list. show_handle_description: Shows a follow button to the left of the "New Topic" button. - show_banner: Show an ActivityPub banner in this category's topic list. - show_banner_description: Shows a banner with ActivityPub statistics above the topic list in this category. username: ActivityPub username for this category. username_description: Cannot be changed once saved. handle: ActivityPub handle for this category. diff --git a/plugin.rb b/plugin.rb index 07015816..168cd94c 100644 --- a/plugin.rb +++ b/plugin.rb @@ -122,7 +122,6 @@ register_category_custom_field_type("activity_pub_enabled", :boolean) register_category_custom_field_type("activity_pub_show_status", :boolean) register_category_custom_field_type("activity_pub_show_handle", :boolean) - register_category_custom_field_type("activity_pub_show_banner", :boolean) register_category_custom_field_type("activity_pub_username", :string) register_category_custom_field_type("activity_pub_name", :string) register_category_custom_field_type("activity_pub_default_visibility", :string) @@ -145,9 +144,6 @@ add_to_class(:category, :activity_pub_show_handle) do DiscourseActivityPub.enabled && !!custom_fields["activity_pub_show_handle"] end - add_to_class(:category, :activity_pub_show_banner) do - DiscourseActivityPub.enabled && !!custom_fields["activity_pub_show_banner"] - end add_to_class(:category, :activity_pub_ready?) do activity_pub_enabled && activity_pub_actor.present? && activity_pub_actor.persisted? @@ -249,11 +245,6 @@ :activity_pub_show_handle, include_condition: -> { object.activity_pub_enabled } ) { object.activity_pub_show_handle } - add_to_serializer( - :basic_category, - :activity_pub_show_banner, - include_condition: -> { object.activity_pub_enabled } - ) { object.activity_pub_show_banner } add_to_serializer( :basic_category, :activity_pub_default_visibility, @@ -280,11 +271,9 @@ Site.preloaded_category_custom_fields << "activity_pub_ready" Site.preloaded_category_custom_fields << "activity_pub_show_status" Site.preloaded_category_custom_fields << "activity_pub_show_handle" - Site.preloaded_category_custom_fields << "activity_pub_show_banner" Site.preloaded_category_custom_fields << "activity_pub_username" Site.preloaded_category_custom_fields << "activity_pub_name" Site.preloaded_category_custom_fields << "activity_pub_default_visibility" - Site.preloaded_category_custom_fields << "activity_pub_post_object_type" Site.preloaded_category_custom_fields << "activity_pub_publication_type" end From 7309649713f670a0f32c4e9be45d42093a902049 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Mon, 30 Oct 2023 15:12:06 +0800 Subject: [PATCH 04/71] Linting fixes --- .../activity-pub-category-route-map.js | 6 ++-- .../components/activity-pub-follow-domain.js | 35 ++++++++++--------- .../components/activity-pub-follower-card.js | 8 ++--- .../components/activity-pub-status.js | 2 +- .../components/modal/activity-pub-follow.js | 3 +- .../helpers/activity-pub-actor-image.js | 2 +- .../initializers/activity-pub-initializer.js | 14 ++++---- .../discourse/lib/activity-pub-utilities.js | 3 +- .../routes/activity-pub-category-followers.js | 4 +-- 9 files changed, 40 insertions(+), 37 deletions(-) diff --git a/assets/javascripts/discourse/activity-pub-category-route-map.js b/assets/javascripts/discourse/activity-pub-category-route-map.js index b2840fbc..1568c10e 100644 --- a/assets/javascripts/discourse/activity-pub-category-route-map.js +++ b/assets/javascripts/discourse/activity-pub-category-route-map.js @@ -1,5 +1,5 @@ -export default function() { +export default function () { this.route("activityPub.category.followers", { - path: "/ap/category/:category_id/followers" + path: "/ap/category/:category_id/followers", }); -}; \ No newline at end of file +} diff --git a/assets/javascripts/discourse/components/activity-pub-follow-domain.js b/assets/javascripts/discourse/components/activity-pub-follow-domain.js index 17d98308..79df8079 100644 --- a/assets/javascripts/discourse/components/activity-pub-follow-domain.js +++ b/assets/javascripts/discourse/components/activity-pub-follow-domain.js @@ -3,8 +3,9 @@ import { action } from "@ember/object"; import { inject as service } from "@ember/service"; import { tracked } from "@glimmer/tracking"; import { ajax } from "discourse/lib/ajax"; -import { hostnameValid, extractDomainFromUrl } from "discourse/lib/utilities"; +import { extractDomainFromUrl, hostnameValid } from "discourse/lib/utilities"; import { buildHandle } from "../lib/activity-pub-utilities"; +import { Promise } from "rsvp"; import I18n from "I18n"; // We're using a hardcoded url here as mastodon will only webfinger this as @@ -12,11 +13,13 @@ import I18n from "I18n"; // See https://docs.joinmastodon.org/spec/webfinger // See https://socialhub.activitypub.rocks/t/what-is-the-current-spec-for-remote-follow/2020 const mastodonFollowUrl = (domain, handle) => { - return `https://${domain}/authorize_interaction?uri=${encodeURIComponent(handle)}`; + return `https://${domain}/authorize_interaction?uri=${encodeURIComponent( + handle + )}`; }; // See https://docs.joinmastodon.org/methods/instance/#v2 -const mastodonAboutPath = 'api/v2/instance'; +const mastodonAboutPath = "api/v2/instance"; export default class ActivityPubFollowMastodon extends Component { @service site; @@ -24,9 +27,9 @@ export default class ActivityPubFollowMastodon extends Component { @tracked error = null; get footerClass() { - let result = 'activity-pub-follow-domain-footer'; + let result = "activity-pub-follow-domain-footer"; if (this.error) { - result += ' error'; + result += " error"; } return result; } @@ -38,17 +41,17 @@ export default class ActivityPubFollowMastodon extends Component { } return ajax(`https://${domain}/${mastodonAboutPath}`, { - type: 'GET', - ignoreUnsent: false - }).then(response => { - if (response?.domain && response.domain === domain) { - return resolve(mastodonFollowUrl(domain, handle)); - } else { - return resolve(null); - } - }).catch(error => { - return resolve(null); + type: "GET", + ignoreUnsent: false, }) + .then((response) => { + if (response?.domain && response.domain === domain) { + return resolve(mastodonFollowUrl(domain, handle)); + } else { + return resolve(null); + } + }) + .catch(() => resolve(null)); }); } @@ -82,7 +85,7 @@ export default class ActivityPubFollowMastodon extends Component { this.verifying = false; if (url) { - window.open(url, '_blank').focus(); + window.open(url, "_blank").focus(); } else { this.error = I18n.t("discourse_activity_pub.follow.domain.invalid"); } diff --git a/assets/javascripts/discourse/components/activity-pub-follower-card.js b/assets/javascripts/discourse/components/activity-pub-follower-card.js index 825b4a82..b72d803f 100644 --- a/assets/javascripts/discourse/components/activity-pub-follower-card.js +++ b/assets/javascripts/discourse/components/activity-pub-follower-card.js @@ -6,15 +6,15 @@ export default class ActivityPubFollowerCard extends Component { @service site; get imageSize() { - return this.site.mobileView ? 'small' : 'huge'; + return this.site.mobileView ? "small" : "huge"; } get classes() { - let result = 'activity-pub-follower-card'; + let result = "activity-pub-follower-card"; if (this.site.mobileView) { - result += ' mobile'; + result += " mobile"; } else { - result += ' desktop'; + result += " desktop"; } return result; } diff --git a/assets/javascripts/discourse/components/activity-pub-status.js b/assets/javascripts/discourse/components/activity-pub-status.js index 56749295..a77f3665 100644 --- a/assets/javascripts/discourse/components/activity-pub-status.js +++ b/assets/javascripts/discourse/components/activity-pub-status.js @@ -104,7 +104,7 @@ export default class ActivityPubStatus extends Component { get classes() { let result = `activity-pub-status ${this.statusClass}`; if (this.args.onClick) { - result += ' clickable'; + result += " clickable"; } return result; } diff --git a/assets/javascripts/discourse/components/modal/activity-pub-follow.js b/assets/javascripts/discourse/components/modal/activity-pub-follow.js index 9f8db709..184cb844 100644 --- a/assets/javascripts/discourse/components/modal/activity-pub-follow.js +++ b/assets/javascripts/discourse/components/modal/activity-pub-follow.js @@ -4,8 +4,7 @@ import I18n from "I18n"; export default class ActivityPubFollow extends Component { get title() { return I18n.t("discourse_activity_pub.follow.title", { - name: this.args.model.name + name: this.args.model.name, }); } - } diff --git a/assets/javascripts/discourse/helpers/activity-pub-actor-image.js b/assets/javascripts/discourse/helpers/activity-pub-actor-image.js index 673c5aca..5a63fd16 100644 --- a/assets/javascripts/discourse/helpers/activity-pub-actor-image.js +++ b/assets/javascripts/discourse/helpers/activity-pub-actor-image.js @@ -1,5 +1,5 @@ import { htmlSafe } from "@ember/template"; -import { translateSize, avatarUrl } from "discourse-common/lib/avatar-utils"; +import { translateSize } from "discourse-common/lib/avatar-utils"; import { registerRawHelper } from "discourse-common/lib/helpers"; import { buildHandle } from "../lib/activity-pub-utilities"; diff --git a/assets/javascripts/discourse/initializers/activity-pub-initializer.js b/assets/javascripts/discourse/initializers/activity-pub-initializer.js index de64f62a..3c23a120 100644 --- a/assets/javascripts/discourse/initializers/activity-pub-initializer.js +++ b/assets/javascripts/discourse/initializers/activity-pub-initializer.js @@ -3,6 +3,7 @@ import { bind } from "discourse-common/utils/decorators"; import { Promise } from "rsvp"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { ajax } from "discourse/lib/ajax"; +import I18n from "I18n"; export default { name: "activity-pub", @@ -253,12 +254,13 @@ export default { api.addNavigationBarItem({ name: "followers", - title: I18n.t('discourse_activity_pub.followers.description'), - displayName: I18n.t('discourse_activity_pub.followers.label'), - customFilter: (category, args, router) => (category?.activity_pub_enabled), - customHref: (category, args, router) => (`/ap/category/${category.id}/followers`), - forceActive: (category, args, router) => router.currentURL === `/ap/category/${category.id}/followers` - }) + title: I18n.t("discourse_activity_pub.followers.description"), + displayName: I18n.t("discourse_activity_pub.followers.label"), + customFilter: (category) => category?.activity_pub_enabled, + customHref: (category) => `/ap/category/${category.id}/followers`, + forceActive: (category, _, router) => + router.currentURL === `/ap/category/${category.id}/followers`, + }); }); }, }; diff --git a/assets/javascripts/discourse/lib/activity-pub-utilities.js b/assets/javascripts/discourse/lib/activity-pub-utilities.js index 397dc133..f5f57163 100644 --- a/assets/javascripts/discourse/lib/activity-pub-utilities.js +++ b/assets/javascripts/discourse/lib/activity-pub-utilities.js @@ -1,4 +1,3 @@ - export function buildHandle({ actor, model, site }) { if ((!actor && !model) || (model && !site)) { return undefined; @@ -7,4 +6,4 @@ export function buildHandle({ actor, model, site }) { const domain = actor ? actor.domain : site.activity_pub_host; return `${username}@${domain}`; } -} \ No newline at end of file +} diff --git a/assets/javascripts/discourse/routes/activity-pub-category-followers.js b/assets/javascripts/discourse/routes/activity-pub-category-followers.js index e0db293a..0baf3588 100644 --- a/assets/javascripts/discourse/routes/activity-pub-category-followers.js +++ b/assets/javascripts/discourse/routes/activity-pub-category-followers.js @@ -8,14 +8,14 @@ export default DiscourseRoute.extend({ order: { refreshModel: true }, asc: { refreshModel: true }, domain: { refreshModel: true }, - username: { refreshModel: true } + username: { refreshModel: true }, }, model(params) { const category = Category.findById(params.category_id); return ajax(`/ap/category/${category.id}/followers.json`) - .then(response => ({ category, ...response })) + .then((response) => ({ category, ...response })) .catch(popupAjaxError); }, From 1bc0d23e13dbde311b72f8c400b0038f94e010b8 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Mon, 30 Oct 2023 15:21:46 +0800 Subject: [PATCH 05/71] More linting fixes --- assets/javascripts/discourse/components/activity-pub-status.hbs | 2 +- assets/stylesheets/common/common.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/javascripts/discourse/components/activity-pub-status.hbs b/assets/javascripts/discourse/components/activity-pub-status.hbs index 201c03db..a615dc81 100644 --- a/assets/javascripts/discourse/components/activity-pub-status.hbs +++ b/assets/javascripts/discourse/components/activity-pub-status.hbs @@ -1,4 +1,4 @@ -
+
{{d-icon "discourse-activity-pub"}} {{this.translatedLabel}}
diff --git a/assets/stylesheets/common/common.scss b/assets/stylesheets/common/common.scss index 83cead04..655fd498 100644 --- a/assets/stylesheets/common/common.scss +++ b/assets/stylesheets/common/common.scss @@ -416,4 +416,4 @@ body.user-preferences-activity-pub-page { &.error { color: var(--danger); } -} \ No newline at end of file +} From 6ab41a24f02f3ea16ea635857af6c72f51680a52 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Mon, 30 Oct 2023 15:45:26 +0800 Subject: [PATCH 06/71] Fix failing specs --- plugin.rb | 3 ++- .../category_controller_spec.rb | 12 ++++++------ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/plugin.rb b/plugin.rb index 168cd94c..a6ffde99 100644 --- a/plugin.rb +++ b/plugin.rb @@ -258,7 +258,7 @@ add_to_serializer( :basic_category, :activity_pub_post_object_type, - include_condition: -> { object.activity_pub_post_object_type } + include_condition: -> { object.activity_pub_enabled } ) { object.activity_pub_post_object_type } add_to_serializer( :basic_category, @@ -275,6 +275,7 @@ Site.preloaded_category_custom_fields << "activity_pub_name" Site.preloaded_category_custom_fields << "activity_pub_default_visibility" Site.preloaded_category_custom_fields << "activity_pub_publication_type" + Site.preloaded_category_custom_fields << "activity_pub_post_object_type" end Topic.has_one :activity_pub_object, diff --git a/spec/requests/discourse_activity_pub/category_controller_spec.rb b/spec/requests/discourse_activity_pub/category_controller_spec.rb index 75f79cb0..6a2b7d25 100644 --- a/spec/requests/discourse_activity_pub/category_controller_spec.rb +++ b/spec/requests/discourse_activity_pub/category_controller_spec.rb @@ -19,15 +19,15 @@ def build_error(key) end it "returns the categories followers" do - get "/c/#{actor.model.slug}/#{actor.model.id}/ap/followers.json" + get "/ap/category/#{actor.model.id}/followers.json" expect(response.status).to eq(200) - expect(response.parsed_body['followers'].map{|f| f["ap_id"] }).to eq( + expect(response.parsed_body['followers'].map{|f| f["url"] }).to eq( [follower3.ap_id, follower2.ap_id, follower1.ap_id] ) end it "orders by ap domain" do - get "/c/#{actor.model.slug}/#{actor.model.id}/ap/followers.json?order=domain" + get "/ap/category/#{actor.model.id}/followers.json?order=domain" expect(response.status).to eq(200) expect(response.parsed_body['followers'].map{|f| f["domain"] }).to eq( ["twitter.com", "netflix.com", "google.com"] @@ -35,7 +35,7 @@ def build_error(key) end it "orders by ap username" do - get "/c/#{actor.model.slug}/#{actor.model.id}/ap/followers.json?order=username" + get "/ap/category/#{actor.model.id}/followers.json?order=username" expect(response.status).to eq(200) expect(response.parsed_body['followers'].map{|f| f["username"] }).to eq( ["xavier", "jenny", "bob"] @@ -43,9 +43,9 @@ def build_error(key) end it "paginates" do - get "/c/#{actor.model.slug}/#{actor.model.id}/ap/followers.json?limit=2&page=1" + get "/ap/category/#{actor.model.id}/followers.json?limit=2&page=1" expect(response.status).to eq(200) - expect(response.parsed_body['followers'].map{|f| f["ap_id"] }).to eq( + expect(response.parsed_body['followers'].map{|f| f["url"] }).to eq( [follower1.ap_id] ) end From 23436a0dbc26090aaad810ffc95e37a2e3032e71 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Tue, 31 Oct 2023 15:35:22 +0800 Subject: [PATCH 07/71] Move to table in followers route --- .../category_controller.rb | 2 +- .../follower_serializer.rb | 3 +- .../activity-pub-category-banner.hbs | 3 +- .../components/activity-pub-follow-btn.hbs | 16 +-- .../components/activity-pub-follower-card.hbs | 32 ------ .../components/activity-pub-follower-card.js | 21 ---- .../components/activity-pub-followers.hbs | 5 - .../components/activity-pub-followers.js | 3 - .../components/activity-pub-nav-item.hbs | 8 ++ .../components/activity-pub-nav-item.js | 23 ++++ .../components/activity-pub-status.hbs | 2 +- .../components/activity-pub-status.js | 18 +-- .../discovery-activity-pub-handle.hbs | 3 - .../discovery-activity-pub-handle.js | 5 - .../activity-pub-category-navigation.hbs | 0 .../activity-pub-category-navigation.hbs | 1 + .../activity-pub-category-navigation.js | 0 .../activity-pub-category-followers.js | 13 +++ .../helpers/activity-pub-actor-handle.js | 16 +++ .../initializers/activity-pub-initializer.js | 11 -- .../models/activity-pub-followers.js | 48 ++++++++ .../routes/activity-pub-category-followers.js | 19 +-- .../activity-pub-category-followers.hbs | 79 ++++++++++++- assets/stylesheets/common/common.scss | 108 ++---------------- config/locales/client.en.yml | 8 +- 25 files changed, 219 insertions(+), 228 deletions(-) delete mode 100644 assets/javascripts/discourse/components/activity-pub-follower-card.hbs delete mode 100644 assets/javascripts/discourse/components/activity-pub-follower-card.js delete mode 100644 assets/javascripts/discourse/components/activity-pub-followers.hbs delete mode 100644 assets/javascripts/discourse/components/activity-pub-followers.js create mode 100644 assets/javascripts/discourse/components/activity-pub-nav-item.hbs create mode 100644 assets/javascripts/discourse/components/activity-pub-nav-item.js delete mode 100644 assets/javascripts/discourse/connectors/before-create-topic-button/discovery-activity-pub-handle.hbs delete mode 100644 assets/javascripts/discourse/connectors/before-create-topic-button/discovery-activity-pub-handle.js delete mode 100644 assets/javascripts/discourse/connectors/category-navigation/activity-pub-category-navigation.hbs create mode 100644 assets/javascripts/discourse/connectors/extra-nav-item/activity-pub-category-navigation.hbs rename assets/javascripts/discourse/connectors/{category-navigation => extra-nav-item}/activity-pub-category-navigation.js (100%) create mode 100644 assets/javascripts/discourse/helpers/activity-pub-actor-handle.js create mode 100644 assets/javascripts/discourse/models/activity-pub-followers.js diff --git a/app/controllers/discourse_activity_pub/category_controller.rb b/app/controllers/discourse_activity_pub/category_controller.rb index c571b4e7..c228cae8 100644 --- a/app/controllers/discourse_activity_pub/category_controller.rb +++ b/app/controllers/discourse_activity_pub/category_controller.rb @@ -41,7 +41,7 @@ def followers followers: serialized, meta: { total: total, - load_more: load_more_uri.to_s, + load_more_url: load_more_uri.to_s, } ) end diff --git a/app/serializers/discourse_activity_pub/follower_serializer.rb b/app/serializers/discourse_activity_pub/follower_serializer.rb index 2b5b1940..eee19b97 100644 --- a/app/serializers/discourse_activity_pub/follower_serializer.rb +++ b/app/serializers/discourse_activity_pub/follower_serializer.rb @@ -2,7 +2,8 @@ module DiscourseActivityPub class FollowerSerializer < ActiveModel::Serializer - attributes :username, + attributes :name, + :username, :local, :domain, :url, diff --git a/assets/javascripts/discourse/components/activity-pub-category-banner.hbs b/assets/javascripts/discourse/components/activity-pub-category-banner.hbs index 4d83e77a..e5c9f988 100644 --- a/assets/javascripts/discourse/components/activity-pub-category-banner.hbs +++ b/assets/javascripts/discourse/components/activity-pub-category-banner.hbs @@ -1,8 +1,9 @@
{{#if @category}} - + {{#unless this.site.mobileView}} {{/unless}} + {{/if}}
\ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-follow-btn.hbs b/assets/javascripts/discourse/components/activity-pub-follow-btn.hbs index 5a1419ec..bc41af15 100644 --- a/assets/javascripts/discourse/components/activity-pub-follow-btn.hbs +++ b/assets/javascripts/discourse/components/activity-pub-follow-btn.hbs @@ -1,10 +1,6 @@ -
- -
\ No newline at end of file + \ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-follower-card.hbs b/assets/javascripts/discourse/components/activity-pub-follower-card.hbs deleted file mode 100644 index 5ea4e7d4..00000000 --- a/assets/javascripts/discourse/components/activity-pub-follower-card.hbs +++ /dev/null @@ -1,32 +0,0 @@ -
-
-
- {{activityPubActorImage @follower size=imageSize classes=imageClasses}} -
-
- -
- {{#if @follower.user}} - - {{/if}} -
-
-
-
- -
{{i18n "discourse_activity_pub.follower.followed_at"}}
-
{{bound-date @follower.followed_at}}
-
-
-
-
-
\ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-follower-card.js b/assets/javascripts/discourse/components/activity-pub-follower-card.js deleted file mode 100644 index b72d803f..00000000 --- a/assets/javascripts/discourse/components/activity-pub-follower-card.js +++ /dev/null @@ -1,21 +0,0 @@ -import Component from "@glimmer/component"; -import { inject as service } from "@ember/service"; - -export default class ActivityPubFollowerCard extends Component { - @service siteSettings; - @service site; - - get imageSize() { - return this.site.mobileView ? "small" : "huge"; - } - - get classes() { - let result = "activity-pub-follower-card"; - if (this.site.mobileView) { - result += " mobile"; - } else { - result += " desktop"; - } - return result; - } -} diff --git a/assets/javascripts/discourse/components/activity-pub-followers.hbs b/assets/javascripts/discourse/components/activity-pub-followers.hbs deleted file mode 100644 index c91f7825..00000000 --- a/assets/javascripts/discourse/components/activity-pub-followers.hbs +++ /dev/null @@ -1,5 +0,0 @@ -
- {{#each @followers as |follower|}} - - {{/each}} -
\ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-followers.js b/assets/javascripts/discourse/components/activity-pub-followers.js deleted file mode 100644 index ed53e513..00000000 --- a/assets/javascripts/discourse/components/activity-pub-followers.js +++ /dev/null @@ -1,3 +0,0 @@ -import Component from "@glimmer/component"; - -export default class ActivityPubFollowers extends Component {} diff --git a/assets/javascripts/discourse/components/activity-pub-nav-item.hbs b/assets/javascripts/discourse/components/activity-pub-nav-item.hbs new file mode 100644 index 00000000..9a5a9d6b --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-nav-item.hbs @@ -0,0 +1,8 @@ + + {{d-icon "discourse-activity-pub"}} + {{i18n 'discourse_activity_pub.followers.label'}} + \ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-nav-item.js b/assets/javascripts/discourse/components/activity-pub-nav-item.js new file mode 100644 index 00000000..64fbe213 --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-nav-item.js @@ -0,0 +1,23 @@ +import Component from "@glimmer/component"; +import { inject as service } from "@ember/service"; +import getURL from "discourse-common/lib/get-url"; + +export default class ActivityPubNavItem extends Component { + @service router; + + get classes() { + let result = ""; + if (this.active) { + result += " active"; + } + return result; + } + + get href() { + return getURL(`/ap/category/${this.args.category.id}/followers`); + } + + get active() { + return this.router.currentRouteName === "activityPub.category.followers"; + } +} diff --git a/assets/javascripts/discourse/components/activity-pub-status.hbs b/assets/javascripts/discourse/components/activity-pub-status.hbs index a615dc81..b0ce33f2 100644 --- a/assets/javascripts/discourse/components/activity-pub-status.hbs +++ b/assets/javascripts/discourse/components/activity-pub-status.hbs @@ -1,4 +1,4 @@ -
+
{{d-icon "discourse-activity-pub"}} {{this.translatedLabel}}
diff --git a/assets/javascripts/discourse/components/activity-pub-status.js b/assets/javascripts/discourse/components/activity-pub-status.js index a77f3665..48d7c121 100644 --- a/assets/javascripts/discourse/components/activity-pub-status.js +++ b/assets/javascripts/discourse/components/activity-pub-status.js @@ -1,7 +1,6 @@ import Component from "@glimmer/component"; import { tracked } from "@glimmer/tracking"; import { inject as service } from "@ember/service"; -import { action } from "@ember/object"; import { bind } from "discourse-common/utils/decorators"; import I18n from "I18n"; @@ -57,10 +56,6 @@ export default class ActivityPubStatus extends Component { } get translatedTitle() { - if (this.args.translatedTitle) { - return this.args.translatedTitle; - } - const args = { model_type: this.args.modelType, }; @@ -122,17 +117,6 @@ export default class ActivityPubStatus extends Component { } get translatedLabel() { - if (this.args.translatedLabel) { - return this.args.translatedLabel; - } else { - return I18n.t(this.labelKey("label")); - } - } - - @action - click(event) { - if (this.args.onClick) { - this.args.onClick(event); - } + return I18n.t(this.labelKey("label")); } } diff --git a/assets/javascripts/discourse/connectors/before-create-topic-button/discovery-activity-pub-handle.hbs b/assets/javascripts/discourse/connectors/before-create-topic-button/discovery-activity-pub-handle.hbs deleted file mode 100644 index 20412146..00000000 --- a/assets/javascripts/discourse/connectors/before-create-topic-button/discovery-activity-pub-handle.hbs +++ /dev/null @@ -1,3 +0,0 @@ -{{#if this.category.activity_pub_show_handle}} - -{{/if}} \ No newline at end of file diff --git a/assets/javascripts/discourse/connectors/before-create-topic-button/discovery-activity-pub-handle.js b/assets/javascripts/discourse/connectors/before-create-topic-button/discovery-activity-pub-handle.js deleted file mode 100644 index a45ffb36..00000000 --- a/assets/javascripts/discourse/connectors/before-create-topic-button/discovery-activity-pub-handle.js +++ /dev/null @@ -1,5 +0,0 @@ -export default { - shouldRender(_, ctx) { - return ctx.site.activity_pub_enabled; - }, -}; diff --git a/assets/javascripts/discourse/connectors/category-navigation/activity-pub-category-navigation.hbs b/assets/javascripts/discourse/connectors/category-navigation/activity-pub-category-navigation.hbs deleted file mode 100644 index e69de29b..00000000 diff --git a/assets/javascripts/discourse/connectors/extra-nav-item/activity-pub-category-navigation.hbs b/assets/javascripts/discourse/connectors/extra-nav-item/activity-pub-category-navigation.hbs new file mode 100644 index 00000000..97b12bbf --- /dev/null +++ b/assets/javascripts/discourse/connectors/extra-nav-item/activity-pub-category-navigation.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/javascripts/discourse/connectors/category-navigation/activity-pub-category-navigation.js b/assets/javascripts/discourse/connectors/extra-nav-item/activity-pub-category-navigation.js similarity index 100% rename from assets/javascripts/discourse/connectors/category-navigation/activity-pub-category-navigation.js rename to assets/javascripts/discourse/connectors/extra-nav-item/activity-pub-category-navigation.js diff --git a/assets/javascripts/discourse/controllers/activity-pub-category-followers.js b/assets/javascripts/discourse/controllers/activity-pub-category-followers.js index 1b2c72b5..2f68e759 100644 --- a/assets/javascripts/discourse/controllers/activity-pub-category-followers.js +++ b/assets/javascripts/discourse/controllers/activity-pub-category-followers.js @@ -1,6 +1,19 @@ import Controller from "@ember/controller"; import { inject as service } from "@ember/service"; +import { tracked } from "@glimmer/tracking"; +import { action } from "@ember/object"; export default class ActivityPubCategoryFollowers extends Controller { @service composer; + @service siteSettings; + + @tracked order = ""; + @tracked asc = null; + + queryParams = ["order", "asc"]; + + @action + loadMore() { + this.model.loadMore(); + } } diff --git a/assets/javascripts/discourse/helpers/activity-pub-actor-handle.js b/assets/javascripts/discourse/helpers/activity-pub-actor-handle.js new file mode 100644 index 00000000..a7b29be3 --- /dev/null +++ b/assets/javascripts/discourse/helpers/activity-pub-actor-handle.js @@ -0,0 +1,16 @@ +import { htmlSafe } from "@ember/template"; +import { registerRawHelper } from "discourse-common/lib/helpers"; +import { buildHandle } from "../lib/activity-pub-utilities"; + +function renderActivityPubActorHandle(actor) { + if (actor) { + const handle = buildHandle({ actor }); + return `${handle}`; + } +} + +registerRawHelper("activityPubActorHandle", activityPubActorHandle); + +export default function activityPubActorHandle(actor) { + return htmlSafe(renderActivityPubActorHandle.call(this, actor)); +} diff --git a/assets/javascripts/discourse/initializers/activity-pub-initializer.js b/assets/javascripts/discourse/initializers/activity-pub-initializer.js index 3c23a120..9bef9f1f 100644 --- a/assets/javascripts/discourse/initializers/activity-pub-initializer.js +++ b/assets/javascripts/discourse/initializers/activity-pub-initializer.js @@ -3,7 +3,6 @@ import { bind } from "discourse-common/utils/decorators"; import { Promise } from "rsvp"; import { popupAjaxError } from "discourse/lib/ajax-error"; import { ajax } from "discourse/lib/ajax"; -import I18n from "I18n"; export default { name: "activity-pub", @@ -251,16 +250,6 @@ export default { ); }, }); - - api.addNavigationBarItem({ - name: "followers", - title: I18n.t("discourse_activity_pub.followers.description"), - displayName: I18n.t("discourse_activity_pub.followers.label"), - customFilter: (category) => category?.activity_pub_enabled, - customHref: (category) => `/ap/category/${category.id}/followers`, - forceActive: (category, _, router) => - router.currentURL === `/ap/category/${category.id}/followers`, - }); }); }, }; diff --git a/assets/javascripts/discourse/models/activity-pub-followers.js b/assets/javascripts/discourse/models/activity-pub-followers.js new file mode 100644 index 00000000..7c9baaad --- /dev/null +++ b/assets/javascripts/discourse/models/activity-pub-followers.js @@ -0,0 +1,48 @@ +import EmberObject from "@ember/object"; +import { ajax } from "discourse/lib/ajax"; +import { popupAjaxError } from "discourse/lib/ajax-error"; + +const ActivityPubFollowers = EmberObject.extend({ + loadMore() { + if (!this.loadMoreUrl || this.total <= this.followers.length) { + return; + } + + this.set("loadingMore", true); + + return ajax(this.loadMoreUrl) + .then((response) => { + if (response) { + this.followers.pushObjects(response.followers); + this.setProperties({ + loadMoreUrl: response.meta.load_more_url, + loadingMore: false, + }); + } + }) + .catch(popupAjaxError); + }, +}); + +ActivityPubFollowers.reopenClass({ + load(category, params) { + const queryParams = new URLSearchParams(); + + if (params.order) { + queryParams.set("order", params.order); + } + + if (params.asc) { + queryParams.set("asc", params.asc); + } + + const path = `/ap/category/${category.id}/followers`; + const url = `${path}.json?${queryParams.toString()}`; + + return ajax(url) + .then((response) => ({ category, ...response })) + .catch(popupAjaxError); + }, +}); + +export default ActivityPubFollowers; diff --git a/assets/javascripts/discourse/routes/activity-pub-category-followers.js b/assets/javascripts/discourse/routes/activity-pub-category-followers.js index 0baf3588..4e38db6b 100644 --- a/assets/javascripts/discourse/routes/activity-pub-category-followers.js +++ b/assets/javascripts/discourse/routes/activity-pub-category-followers.js @@ -1,25 +1,26 @@ -import { ajax } from "discourse/lib/ajax"; -import { popupAjaxError } from "discourse/lib/ajax-error"; import DiscourseRoute from "discourse/routes/discourse"; import Category from "discourse/models/category"; +import { A } from "@ember/array"; +import ActivityPubFollowers from "../models/activity-pub-followers"; export default DiscourseRoute.extend({ queryParams: { order: { refreshModel: true }, asc: { refreshModel: true }, - domain: { refreshModel: true }, - username: { refreshModel: true }, }, model(params) { const category = Category.findById(params.category_id); - - return ajax(`/ap/category/${category.id}/followers.json`) - .then((response) => ({ category, ...response })) - .catch(popupAjaxError); + return ActivityPubFollowers.load(category, params); }, setupController(controller, model) { - controller.setProperties(model); + model = ActivityPubFollowers.create({ + category: model.category, + followers: A(model.followers), + loadMoreUrl: model.meta.load_more_url, + total: model.meta.total, + }); + controller.setProperties({ model }); }, }); diff --git a/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs b/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs index 2310e104..991a0d73 100644 --- a/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs +++ b/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs @@ -1,8 +1,79 @@ - \ No newline at end of file + @category={{this.model.category}} +/> + + + + + {{#if this.model}} + + <:header> + + + + + <:body> + {{#each this.model.followers as |follower|}} +
+
+ {{activityPubActorImage follower size="large"}} +
+
+ {{or follower.name follower.username}} +
+ {{activityPubActorHandle follower}} +
+
+ +
+ {{bound-date follower.followed_at}} +
+
+ {{/each}} + +
+ + + {{else}} +

{{i18n "search.no_results"}}

+ {{/if}} +
\ No newline at end of file diff --git a/assets/stylesheets/common/common.scss b/assets/stylesheets/common/common.scss index 655fd498..af705fa0 100644 --- a/assets/stylesheets/common/common.scss +++ b/assets/stylesheets/common/common.scss @@ -46,19 +46,6 @@ div.activity-pub-status { } } -.activity-pub-status-tip { - background: var(--primary-low); - align-items: center; - display: flex; - min-height: 30px; - margin-left: 0.5em; - - .d-icon { - color: var(--primary-medium); - font-size: 1rem; - } -} - .activity-pub-setting { margin-top: 4px; @@ -229,10 +216,6 @@ body.user-preferences-activity-pub-page { } } -.activity-pub-category-navigation { - width: 100%; -} - .activity-pub-category-banner { display: flex; align-items: center; @@ -276,96 +259,25 @@ body.user-preferences-activity-pub-page { } .activity-pub-followers { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); - grid-gap: 60px 20px; - margin-top: 60px; - padding: 1em 0; + grid-template-columns: minmax(13em, 3fr) repeat(2, minmax(max-content, 1fr)); } -.activity-pub-follower { - background: var(--secondary); - box-shadow: var(--shadow-card); -} +.activity-pub-follower-actor { + justify-content: flex-start; -.activity-pub-follower-content { - padding: 1em; + .activity-pub-follower-actor-content { + margin-left: 1em; + display: flex; + flex-flow: column; + } } .activity-pub-follower-user { display: flex; align-items: center; - .activity-pub-site-icon { - margin-right: 0.5em; - - img { - width: 24px; - height: 24px; - } - } -} - -.activity-pub-follower-card { - --avatar-width-desktop: 8em; - --avatar-margin-desktop: -3.3em; - --avatar-width-mobile: 4em; - --avatar-margin-mobile: 0; - - box-shadow: var(--shadow-card); - padding: 1em; - display: flex; - max-width: calc(100vw - 20px); - box-sizing: border-box; - - &.desktop { - .activity-pub-actor-image { - margin-top: var(--avatar-margin-desktop); - max-height: var(--avatar-width-desktop); - - img { - width: var(--avatar-width-desktop); - height: var(--avatar-width-desktop); - } - } - } - - &.mobile { - .activity-pub-actor-image { - margin-top: var(--avatar-margin-mobile); - max-height: var(--avatar-width-mobile); - - img { - width: var(--avatar-width-mobile); - height: var(--avatar-width-mobile); - } - } - } -} - -.activity-pub-follower-card-content { - display: flex; - flex-direction: column; - gap: 1em; - max-width: 100%; - - .row { - display: flex; - gap: 1em; - max-width: 100%; - } - - .top-content { - display: flex; - flex-direction: column; - gap: 0.75em; - overflow: hidden; - } -} - -.activity-pub-actor-image { - img { - border-radius: 50%; + .avatar { + margin-right: 0.25em; } } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 8ba320f1..878f5873 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -54,13 +54,9 @@ en: followers: label: Followers description: People following this category via ActivityPub. - label_with_count: - one: "%{count} Follower" - other: "%{count} Followers" - title: - one: "%{count} person is following this category via ActivityPub." - other: "%{count} people are following this category via ActivityPub." follower: + actor: Actor + user: User followed_at: Followed visibility: label: From 309cd5a0406c573d2e059a1a3ff42d735afc43ba Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Tue, 31 Oct 2023 16:31:17 +0800 Subject: [PATCH 08/71] Add activity_pub_post_object_type to serialized category fields --- plugin.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin.rb b/plugin.rb index 26353225..2216668a 100644 --- a/plugin.rb +++ b/plugin.rb @@ -247,6 +247,7 @@ activity_pub_name activity_pub_default_visibility activity_pub_publication_type + activity_pub_post_object_type ) serialized_category_custom_fields.each do |field| add_to_serializer( From a48d7b917ba94a231a3596ada5817c0d2214d201 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Tue, 31 Oct 2023 17:09:54 +0800 Subject: [PATCH 09/71] Improve actor list ordering --- .../category_controller.rb | 18 +++-- app/models/discourse_activity_pub_actor.rb | 1 + .../activity-pub-category-followers.hbs | 4 +- .../category_controller_spec.rb | 72 ++++++++++++++----- 4 files changed, 72 insertions(+), 23 deletions(-) diff --git a/app/controllers/discourse_activity_pub/category_controller.rb b/app/controllers/discourse_activity_pub/category_controller.rb index c228cae8..0ccf159a 100644 --- a/app/controllers/discourse_activity_pub/category_controller.rb +++ b/app/controllers/discourse_activity_pub/category_controller.rb @@ -3,7 +3,7 @@ module DiscourseActivityPub class CategoryController < ApplicationController PAGE_SIZE = 50 - ORDER = %w(domain username followed_at) + ORDER = %w(actor user followed_at) before_action :ensure_site_enabled before_action :find_category @@ -14,16 +14,26 @@ def index def followers guardian.ensure_can_see!(@category) - order_followed_at = params[:order] == 'followed_at' permitted_order = ORDER.find { |attr| attr == params[:order] } - order = (order_followed_at || !permitted_order) ? 'created_at' : permitted_order + order = case permitted_order + when 'actor' then 'username' + when 'user' then 'username' + when 'followed_at' then 'created_at' + else 'created_at' + end + order_table = case permitted_order + when 'actor' then 'discourse_activity_pub_actors' + when 'user' then 'users' + when 'followed_at' then 'discourse_activity_pub_follows' + else 'discourse_activity_pub_follows' + end order_dir = params[:asc] ? "ASC" : "DESC" - order_table = order == 'created_at' ? 'discourse_activity_pub_follows' : 'discourse_activity_pub_actors' followers = @category .activity_pub_followers .joins(:follow_follows) .where(follow_follows: { followed_id: @category.activity_pub_actor.id }) + .joins(:user) .order("#{order_table}.#{order} #{order_dir}") limit = fetch_limit_from_params(default: PAGE_SIZE, max: PAGE_SIZE) diff --git a/app/models/discourse_activity_pub_actor.rb b/app/models/discourse_activity_pub_actor.rb index fcf310bf..41bd6a22 100644 --- a/app/models/discourse_activity_pub_actor.rb +++ b/app/models/discourse_activity_pub_actor.rb @@ -5,6 +5,7 @@ class DiscourseActivityPubActor < ActiveRecord::Base include DiscourseActivityPub::WebfingerActorAttributes belongs_to :model, polymorphic: true, optional: true + belongs_to :user, -> { where(discourse_activity_pub_actors: { model_type: 'User' }) }, foreign_key: 'model_id', optional: true has_many :activities, class_name: "DiscourseActivityPubActivity", foreign_key: "actor_id", dependent: :destroy has_many :follow_followers, class_name: "DiscourseActivityPubFollow", foreign_key: "followed_id", dependent: :destroy diff --git a/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs b/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs index 991a0d73..9dbadbf6 100644 --- a/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs +++ b/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs @@ -17,14 +17,14 @@ <:header> [I18n.t("discourse_activity_pub.auth.error.#{key}")] } - end + let!(:follower1) { + Fabricate(:discourse_activity_pub_actor_person, + domain: 'google.com', + username: 'bob_ap', + model: Fabricate(:user, + username: 'bob_local' + ) + ) + } + let!(:follow1) { + Fabricate(:discourse_activity_pub_follow, + follower: follower1, + followed: actor, + created_at: (DateTime.now - 2) + ) + } + let!(:follower2) { + Fabricate(:discourse_activity_pub_actor_person, + domain: 'twitter.com', + username: 'jenny_ap', + model: Fabricate(:user, + username: 'z_jenny_local' + ) + ) + } + let!(:follow2) { + Fabricate(:discourse_activity_pub_follow, + follower: follower2, + followed: actor, + created_at: (DateTime.now - 1) + ) + } + let!(:follower3) { + Fabricate(:discourse_activity_pub_actor_person, + domain: 'netflix.com', + username: 'xavier_ap', + model: Fabricate(:user, + username: 'xavier_local' + ) + ) + } + let!(:follow3) { + Fabricate(:discourse_activity_pub_follow, + follower: follower3, + followed: actor, + created_at: DateTime.now + ) + } describe "#followers" do before do @@ -26,19 +64,19 @@ def build_error(key) ) end - it "orders by ap domain" do - get "/ap/category/#{actor.model.id}/followers.json?order=domain" + it "orders by user" do + get "/ap/category/#{actor.model.id}/followers.json?order=user" expect(response.status).to eq(200) - expect(response.parsed_body['followers'].map{|f| f["domain"] }).to eq( - ["twitter.com", "netflix.com", "google.com"] + expect(response.parsed_body['followers'].map{|f| f["user"]["username"] }).to eq( + ["z_jenny_local", "xavier_local", "bob_local"] ) end - it "orders by ap username" do - get "/ap/category/#{actor.model.id}/followers.json?order=username" + it "orders by actor" do + get "/ap/category/#{actor.model.id}/followers.json?order=actor" expect(response.status).to eq(200) expect(response.parsed_body['followers'].map{|f| f["username"] }).to eq( - ["xavier", "jenny", "bob"] + ["xavier_ap", "jenny_ap", "bob_ap"] ) end From 68402b6b69d7258c86845ec833b36a76f05e5938 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Tue, 31 Oct 2023 17:16:38 +0800 Subject: [PATCH 10/71] Improve category_controller readability --- .../category_controller.rb | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/app/controllers/discourse_activity_pub/category_controller.rb b/app/controllers/discourse_activity_pub/category_controller.rb index 0ccf159a..3016fea1 100644 --- a/app/controllers/discourse_activity_pub/category_controller.rb +++ b/app/controllers/discourse_activity_pub/category_controller.rb @@ -14,53 +14,62 @@ def index def followers guardian.ensure_can_see!(@category) - permitted_order = ORDER.find { |attr| attr == params[:order] } - order = case permitted_order - when 'actor' then 'username' - when 'user' then 'username' - when 'followed_at' then 'created_at' - else 'created_at' - end - order_table = case permitted_order - when 'actor' then 'discourse_activity_pub_actors' - when 'user' then 'users' - when 'followed_at' then 'discourse_activity_pub_follows' - else 'discourse_activity_pub_follows' - end - order_dir = params[:asc] ? "ASC" : "DESC" - followers = @category .activity_pub_followers .joins(:follow_follows) .where(follow_follows: { followed_id: @category.activity_pub_actor.id }) .joins(:user) - .order("#{order_table}.#{order} #{order_dir}") + .order("#{order_table}.#{order} #{params[:asc] ? "ASC" : "DESC"}") limit = fetch_limit_from_params(default: PAGE_SIZE, max: PAGE_SIZE) page = fetch_int_from_params(:page, default: 0) total = followers.count followers = followers.limit(limit).offset(limit * page).to_a - load_more_params = params.slice(:order, :asc).permit! - load_more_params[:page] = page + 1 - load_more_uri = ::URI.parse("/ap/category/#{params[:category_id]}/followers.json") - load_more_uri.query = ::URI.encode_www_form(load_more_params.to_h) - - serialized = serialize_data(followers, FollowerSerializer, root: false) render_json_dump( - followers: serialized, + followers: serialize_data(followers, FollowerSerializer, root: false), meta: { total: total, - load_more_url: load_more_uri.to_s, + load_more_url: load_more_url(page), } ) end protected + def permitted_order + @permitted_order ||= ORDER.find { |attr| attr == params[:order] } + end + + def order_table + case permitted_order + when 'actor' then 'discourse_activity_pub_actors' + when 'user' then 'users' + when 'followed_at' then 'discourse_activity_pub_follows' + else 'discourse_activity_pub_follows' + end + end + + def order + case permitted_order + when 'actor' then 'username' + when 'user' then 'username' + when 'followed_at' then 'created_at' + else 'created_at' + end + end + + def load_more_url(page) + load_more_params = params.slice(:order, :asc).permit! + load_more_params[:page] = page + 1 + load_more_uri = ::URI.parse("/ap/category/#{params[:category_id]}/followers.json") + load_more_uri.query = ::URI.encode_www_form(load_more_params.to_h) + load_more_uri.to_s + end + def find_category @category = Category.find_by_id(params.require(:category_id)) - return render_category_error("category_not_found", 400) unless @category.present? + render_category_error("category_not_found", 400) unless @category.present? end def ensure_site_enabled From bb63c01a5d3d553a1c142efed0ce6e9159032ace Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Tue, 31 Oct 2023 20:12:16 +0800 Subject: [PATCH 11/71] Only add navitem in AP categories --- .../extra-nav-item/activity-pub-category-navigation.hbs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/javascripts/discourse/connectors/extra-nav-item/activity-pub-category-navigation.hbs b/assets/javascripts/discourse/connectors/extra-nav-item/activity-pub-category-navigation.hbs index 97b12bbf..287669b2 100644 --- a/assets/javascripts/discourse/connectors/extra-nav-item/activity-pub-category-navigation.hbs +++ b/assets/javascripts/discourse/connectors/extra-nav-item/activity-pub-category-navigation.hbs @@ -1 +1,3 @@ - \ No newline at end of file +{{#if this.category.activity_pub_enabled}} + +{{/if}} \ No newline at end of file From a0a11222b9731e049d932c6d1b7a3e96d4e40ea5 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Tue, 31 Oct 2023 20:49:19 +0800 Subject: [PATCH 12/71] Followers table UI tweak --- .../discourse/templates/activity-pub-category-followers.hbs | 3 --- assets/stylesheets/common/common.scss | 6 +----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs b/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs index 9dbadbf6..1c06f48a 100644 --- a/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs +++ b/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs @@ -59,9 +59,6 @@ > {{avatar follower.user imageSize="small"}} - - {{follower.user.username}} - {{/if}}
diff --git a/assets/stylesheets/common/common.scss b/assets/stylesheets/common/common.scss index af705fa0..c3dfb759 100644 --- a/assets/stylesheets/common/common.scss +++ b/assets/stylesheets/common/common.scss @@ -259,7 +259,7 @@ body.user-preferences-activity-pub-page { } .activity-pub-followers { - grid-template-columns: minmax(13em, 3fr) repeat(2, minmax(max-content, 1fr)); + grid-template-columns: minmax(13em, 3fr) repeat(2, 110px); } .activity-pub-follower-actor { @@ -275,10 +275,6 @@ body.user-preferences-activity-pub-page { .activity-pub-follower-user { display: flex; align-items: center; - - .avatar { - margin-right: 0.25em; - } } .activity-pub-actor-statistics { From 13b7e2e66f1985ee9f0cabdbd7ac4b16851e5e33 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Tue, 31 Oct 2023 20:57:40 +0800 Subject: [PATCH 13/71] Add category enabled check to category controller --- .../category_controller.rb | 5 ++ .../category_controller_spec.rb | 62 ++++++++++--------- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/app/controllers/discourse_activity_pub/category_controller.rb b/app/controllers/discourse_activity_pub/category_controller.rb index 3016fea1..7ac07a1c 100644 --- a/app/controllers/discourse_activity_pub/category_controller.rb +++ b/app/controllers/discourse_activity_pub/category_controller.rb @@ -7,6 +7,7 @@ class CategoryController < ApplicationController before_action :ensure_site_enabled before_action :find_category + before_action :ensure_category_enabled def index end @@ -72,6 +73,10 @@ def find_category render_category_error("category_not_found", 400) unless @category.present? end + def ensure_category_enabled + render_category_error("not_enabled", 403) unless @category.activity_pub_enabled + end + def ensure_site_enabled render_category_error("not_enabled", 403) unless DiscourseActivityPub.enabled end diff --git a/spec/requests/discourse_activity_pub/category_controller_spec.rb b/spec/requests/discourse_activity_pub/category_controller_spec.rb index 3e383940..246577e2 100644 --- a/spec/requests/discourse_activity_pub/category_controller_spec.rb +++ b/spec/requests/discourse_activity_pub/category_controller_spec.rb @@ -52,40 +52,42 @@ } describe "#followers" do - before do - toggle_activity_pub(actor.model) - end + context "with activity pub enabled" do + before do + toggle_activity_pub(actor.model) + end - it "returns the categories followers" do - get "/ap/category/#{actor.model.id}/followers.json" - expect(response.status).to eq(200) - expect(response.parsed_body['followers'].map{|f| f["url"] }).to eq( - [follower3.ap_id, follower2.ap_id, follower1.ap_id] - ) - end + it "returns the categories followers" do + get "/ap/category/#{actor.model.id}/followers.json" + expect(response.status).to eq(200) + expect(response.parsed_body['followers'].map{|f| f["url"] }).to eq( + [follower3.ap_id, follower2.ap_id, follower1.ap_id] + ) + end - it "orders by user" do - get "/ap/category/#{actor.model.id}/followers.json?order=user" - expect(response.status).to eq(200) - expect(response.parsed_body['followers'].map{|f| f["user"]["username"] }).to eq( - ["z_jenny_local", "xavier_local", "bob_local"] - ) - end + it "orders by user" do + get "/ap/category/#{actor.model.id}/followers.json?order=user" + expect(response.status).to eq(200) + expect(response.parsed_body['followers'].map{|f| f["user"]["username"] }).to eq( + ["z_jenny_local", "xavier_local", "bob_local"] + ) + end - it "orders by actor" do - get "/ap/category/#{actor.model.id}/followers.json?order=actor" - expect(response.status).to eq(200) - expect(response.parsed_body['followers'].map{|f| f["username"] }).to eq( - ["xavier_ap", "jenny_ap", "bob_ap"] - ) - end + it "orders by actor" do + get "/ap/category/#{actor.model.id}/followers.json?order=actor" + expect(response.status).to eq(200) + expect(response.parsed_body['followers'].map{|f| f["username"] }).to eq( + ["xavier_ap", "jenny_ap", "bob_ap"] + ) + end - it "paginates" do - get "/ap/category/#{actor.model.id}/followers.json?limit=2&page=1" - expect(response.status).to eq(200) - expect(response.parsed_body['followers'].map{|f| f["url"] }).to eq( - [follower1.ap_id] - ) + it "paginates" do + get "/ap/category/#{actor.model.id}/followers.json?limit=2&page=1" + expect(response.status).to eq(200) + expect(response.parsed_body['followers'].map{|f| f["url"] }).to eq( + [follower1.ap_id] + ) + end end end end \ No newline at end of file From 822c9df963e0b926afd4231bef2078b41e9bfd90 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Wed, 1 Nov 2023 10:41:41 +0800 Subject: [PATCH 14/71] Update and fix frontend tests --- Gemfile.lock | 1 + .../follower_serializer.rb | 2 +- .../components/activity-pub-nav-item.js | 2 +- .../activity-pub-visibility-dropdown.js | 2 +- .../discourse-activity-pub-handle.hbs | 3 + .../helpers/activity-pub-actor-image.js | 3 +- .../models/activity-pub-followers.js | 9 +- .../routes/activity-pub-category-followers.js | 13 +- .../activity-pub-category-followers.hbs | 14 +- config/locales/client.en.yml | 4 +- .../acceptance/activity-pub-discovery-test.js | 268 ++++++++++++++---- .../components/activity-pub-status-test.js | 9 +- .../fixtures/category-followers-fixtures.js | 34 +++ 13 files changed, 284 insertions(+), 80 deletions(-) create mode 100644 assets/javascripts/discourse/connectors/before-create-topic-button/discourse-activity-pub-handle.hbs create mode 100644 test/javascripts/fixtures/category-followers-fixtures.js diff --git a/Gemfile.lock b/Gemfile.lock index e394331f..ab27623d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -37,6 +37,7 @@ GEM PLATFORMS arm64-darwin-21 + arm64-darwin-22 x86_64-linux DEPENDENCIES diff --git a/app/serializers/discourse_activity_pub/follower_serializer.rb b/app/serializers/discourse_activity_pub/follower_serializer.rb index eee19b97..d14dfae8 100644 --- a/app/serializers/discourse_activity_pub/follower_serializer.rb +++ b/app/serializers/discourse_activity_pub/follower_serializer.rb @@ -16,7 +16,7 @@ def user end def followed_at - object.follow_follows&.first.created_at + object.follow_follows&.first.followed_at end end end diff --git a/assets/javascripts/discourse/components/activity-pub-nav-item.js b/assets/javascripts/discourse/components/activity-pub-nav-item.js index 64fbe213..899add4e 100644 --- a/assets/javascripts/discourse/components/activity-pub-nav-item.js +++ b/assets/javascripts/discourse/components/activity-pub-nav-item.js @@ -6,7 +6,7 @@ export default class ActivityPubNavItem extends Component { @service router; get classes() { - let result = ""; + let result = "activity-pub-category-nav"; if (this.active) { result += " active"; } diff --git a/assets/javascripts/discourse/components/activity-pub-visibility-dropdown.js b/assets/javascripts/discourse/components/activity-pub-visibility-dropdown.js index 61a39b46..80d1f08f 100644 --- a/assets/javascripts/discourse/components/activity-pub-visibility-dropdown.js +++ b/assets/javascripts/discourse/components/activity-pub-visibility-dropdown.js @@ -1,7 +1,7 @@ import ComboBoxComponent from "select-kit/components/combo-box"; import I18n from "I18n"; import { computed } from "@ember/object"; -import { observes, on } from "discourse-common/utils/decorators"; +import { observes, on } from "@ember-decorators/object"; import { equal } from "@ember/object/computed"; import { scheduleOnce } from "@ember/runloop"; diff --git a/assets/javascripts/discourse/connectors/before-create-topic-button/discourse-activity-pub-handle.hbs b/assets/javascripts/discourse/connectors/before-create-topic-button/discourse-activity-pub-handle.hbs new file mode 100644 index 00000000..35dad939 --- /dev/null +++ b/assets/javascripts/discourse/connectors/before-create-topic-button/discourse-activity-pub-handle.hbs @@ -0,0 +1,3 @@ +{{#if this.category.activity_pub_show_handle}} + +{{/if}} \ No newline at end of file diff --git a/assets/javascripts/discourse/helpers/activity-pub-actor-image.js b/assets/javascripts/discourse/helpers/activity-pub-actor-image.js index 5a63fd16..7594d863 100644 --- a/assets/javascripts/discourse/helpers/activity-pub-actor-image.js +++ b/assets/javascripts/discourse/helpers/activity-pub-actor-image.js @@ -10,8 +10,7 @@ function renderActivityPubActorImage(actor, opts) { const size = translateSize(opts.size); const url = actor.icon_url || "/images/avatar.png"; const title = buildHandle({ actor }); - const img = ``; - return `
${img}
`; + return ``; } else { return ""; } diff --git a/assets/javascripts/discourse/models/activity-pub-followers.js b/assets/javascripts/discourse/models/activity-pub-followers.js index 7c9baaad..dd281325 100644 --- a/assets/javascripts/discourse/models/activity-pub-followers.js +++ b/assets/javascripts/discourse/models/activity-pub-followers.js @@ -1,8 +1,11 @@ import EmberObject from "@ember/object"; import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import { notEmpty } from "@ember/object/computed"; const ActivityPubFollowers = EmberObject.extend({ + hasFollowers: notEmpty("followers"), + loadMore() { if (!this.loadMoreUrl || this.total <= this.followers.length) { return; @@ -37,7 +40,11 @@ ActivityPubFollowers.reopenClass({ } const path = `/ap/category/${category.id}/followers`; - const url = `${path}.json?${queryParams.toString()}`; + + let url = `${path}.json`; + if (queryParams.size) { + url += `?${queryParams.toString()}`; + } return ajax(url) .then((response) => ({ category, ...response })) diff --git a/assets/javascripts/discourse/routes/activity-pub-category-followers.js b/assets/javascripts/discourse/routes/activity-pub-category-followers.js index 4e38db6b..f72a9571 100644 --- a/assets/javascripts/discourse/routes/activity-pub-category-followers.js +++ b/assets/javascripts/discourse/routes/activity-pub-category-followers.js @@ -15,12 +15,13 @@ export default DiscourseRoute.extend({ }, setupController(controller, model) { - model = ActivityPubFollowers.create({ - category: model.category, - followers: A(model.followers), - loadMoreUrl: model.meta.load_more_url, - total: model.meta.total, + controller.setProperties({ + model: ActivityPubFollowers.create({ + category: model.category, + followers: A(model.followers || []), + loadMoreUrl: model.meta?.load_more_url, + total: model.meta?.total, + }), }); - controller.setProperties({ model }); }, }); diff --git a/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs b/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs index 1c06f48a..57be71eb 100644 --- a/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs +++ b/assets/javascripts/discourse/templates/activity-pub-category-followers.hbs @@ -13,7 +13,7 @@ @selector=".directory-table .directory-table__cell" @action={{action "loadMore"}} > - {{#if this.model}} + {{#if this.model.hasFollowers}} <:header> <:body> {{#each this.model.followers as |follower|}} -
+
- {{activityPubActorImage follower size="large"}} +
+ {{activityPubActorImage follower size="large"}} +
{{or follower.name follower.username}}
- {{activityPubActorHandle follower}} +
+ {{activityPubActorHandle follower}} +
@@ -61,7 +65,7 @@ {{/if}}
-
+
{{bound-date follower.followed_at}}
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 878f5873..de002d00 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -63,8 +63,8 @@ en: private: Followers Only public: Public description: - private: "%{object_type}s are addressed to followers." - public: "%{object_type}s are publicly addressed." + private: "%{object_type} is addressed to followers." + public: "%{object_type} is publicly addressed." post_object_type: label: note: Note diff --git a/test/javascripts/acceptance/activity-pub-discovery-test.js b/test/javascripts/acceptance/activity-pub-discovery-test.js index 4fd39b87..79cc18b2 100644 --- a/test/javascripts/acceptance/activity-pub-discovery-test.js +++ b/test/javascripts/acceptance/activity-pub-discovery-test.js @@ -1,59 +1,217 @@ -import { acceptance, exists } from "discourse/tests/helpers/qunit-helpers"; +import { + acceptance, + exists, + query, +} from "discourse/tests/helpers/qunit-helpers"; import { test } from "qunit"; import { visit } from "@ember/test-helpers"; import Category from "discourse/models/category"; +import { default as CategoryFollowers } from "../fixtures/category-followers-fixtures"; +import I18n from "I18n"; -acceptance("Discourse Activity Pub | Discovery", function (needs) { - needs.user(); - needs.site({ activity_pub_enabled: true }); - - test("with a non-category route", async function (assert) { - await visit("/latest"); - - assert.ok( - !exists(".activity-pub-discovery"), - "the discovery button is not visible" - ); - }); - - test("with a category without show handle enabled", async function (assert) { - const category = Category.findById(2); - category.set("activity_pub_show_handle", false); - - await visit(category.url); - - assert.ok( - !exists(".activity-pub-discovery"), - "the discovery button is not visible" - ); - }); - - test("with a category with show handle enabled", async function (assert) { - const category = Category.findById(2); - category.set("activity_pub_show_handle", true); - - await visit(category.url); - - assert.ok( - exists(".activity-pub-discovery"), - "the discovery button is visible" - ); - - await click(".activity-pub-discovery button"); - - assert.ok( - exists(".activity-pub-discovery-dropdown"), - "the discovery dropdown appears properly" - ); - assert.ok( - exists(".activity-pub-discovery-dropdown .activity-pub-handle"), - "the handle appears in the dropdown" - ); - - await click(".d-header"); // click outside - assert.ok( - !exists(".activity-pub-discovery-dropdown"), - "the discovery dropdown disappears properly" - ); - }); -}); +const followersPath = "/ap/category/2/followers"; + +acceptance( + "Discourse Activity Pub | Discovery without site enabled", + function (needs) { + needs.user(); + needs.site({ activity_pub_enabled: false }); + + test("with a non-category route", async function (assert) { + await visit("/latest"); + + assert.ok( + !exists(".activity-pub-discovery"), + "the discovery button is not visible" + ); + }); + + test("with a category route without category enabled", async function (assert) { + const category = Category.findById(2); + + await visit(category.url); + + assert.ok( + !exists(".activity-pub-category-nav"), + "the activitypub nav button is not visible" + ); + }); + + test("with a category route with category enabled", async function (assert) { + const category = Category.findById(2); + category.set("activity_pub_enabled", true); + + await visit(category.url); + + assert.ok( + !exists(".activity-pub-category-nav"), + "the activitypub nav button is not visible" + ); + }); + } +); + +acceptance( + "Discourse Activity Pub | Discovery with site enabled", + function (needs) { + needs.user(); + needs.site({ activity_pub_enabled: true }); + + test("with a non-category route", async function (assert) { + await visit("/latest"); + + assert.ok( + !exists(".activity-pub-discovery"), + "the discovery button is not visible" + ); + }); + + test("with a category route without category enabled", async function (assert) { + const category = Category.findById(2); + + await visit(category.url); + + assert.ok( + !exists(".activity-pub-category-nav"), + "the activitypub nav button is not visible" + ); + }); + + test("with a category route with category enabled", async function (assert) { + const category = Category.findById(2); + category.set("activity_pub_enabled", true); + + await visit(category.url); + + assert.ok( + exists(".activity-pub-category-nav"), + "the activitypub nav button is visible" + ); + }); + + test("with a category route without show handle enabled", async function (assert) { + const category = Category.findById(2); + category.set("activity_pub_show_handle", false); + + await visit(category.url); + + assert.ok( + !exists(".activity-pub-discovery"), + "the discovery button is not visible" + ); + }); + + test("with a category route with show handle enabled", async function (assert) { + const category = Category.findById(2); + category.set("activity_pub_show_handle", true); + + await visit(category.url); + + assert.ok( + exists(".activity-pub-discovery"), + "the discovery button is visible" + ); + + await click(".activity-pub-discovery button"); + + assert.ok( + exists(".activity-pub-discovery-dropdown"), + "the discovery dropdown appears properly" + ); + assert.ok( + exists(".activity-pub-discovery-dropdown .activity-pub-handle"), + "the handle appears in the dropdown" + ); + + await click(".d-header"); // click outside + assert.ok( + !exists(".activity-pub-discovery-dropdown"), + "the discovery dropdown disappears properly" + ); + }); + } +); + +acceptance( + "Discourse Activity Pub | Discovery activitypub category route without followers", + function (needs) { + needs.user(); + needs.site({ activity_pub_enabled: true }); + needs.pretender((server, helper) => { + server.get(`${followersPath}.json`, () => + helper.response({ followers: [] }) + ); + }); + + test("with category enabled", async function (assert) { + const category = Category.findById(2); + category.set("activity_pub_enabled", true); + + await visit(followersPath); + + assert.ok( + !exists(".activity-pub-followers"), + "the activitypub followers table is not visible" + ); + assert.equal( + query(".activity-pub-followers-container").innerText, + I18n.t("search.no_results"), + "no results shown" + ); + }); + } +); + +acceptance( + "Discourse Activity Pub | Discovery activitypub category route with followers", + function (needs) { + needs.user(); + needs.site({ activity_pub_enabled: true }); + needs.pretender((server, helper) => { + const path = `${followersPath}.json`; + server.get(path, () => helper.response(CategoryFollowers[path])); + }); + + test("with category enabled", async function (assert) { + const category = Category.findById(2); + category.set("activity_pub_enabled", true); + + await visit(followersPath); + + assert.ok( + exists(".activity-pub-followers"), + "the activitypub followers table is visible" + ); + assert.strictEqual( + document.querySelectorAll(".activity-pub-follower").length, + 2, + "followers are visible" + ); + assert.ok( + query(".activity-pub-follower-image img").src.includes( + "/images/avatar.png" + ), + "follower image is visible" + ); + assert.equal( + query(".activity-pub-follower-name").innerText, + "Angus", + "follower name is visible" + ); + assert.equal( + query(".activity-pub-follower-handle").innerText, + "angus_ap@test.local", + "follower handle is visible" + ); + assert.ok( + query(".activity-pub-follower-user a.avatar").href.includes("/u/angus"), + "follower user avatar is visible" + ); + assert.equal( + query(".activity-pub-followed-at").innerText, + "Feb 9, '13", + "follower followed at is visible" + ); + }); + } +); diff --git a/test/javascripts/components/activity-pub-status-test.js b/test/javascripts/components/activity-pub-status-test.js index 22f857a5..80fd4d05 100644 --- a/test/javascripts/components/activity-pub-status-test.js +++ b/test/javascripts/components/activity-pub-status-test.js @@ -10,7 +10,7 @@ import { module, test } from "qunit"; import I18n from "I18n"; import Site from "discourse/models/site"; import AppEvents from "discourse/services/app-events"; -import { getOwner } from "discourse-common/lib/get-owner"; +import { getOwner } from "@ember/application"; function setSite(context, attrs = {}) { context.siteSettings.activity_pub_enabled = attrs.activity_pub_enabled; @@ -35,11 +35,8 @@ function setCategory(context, attrs = {}) { function setComposer(context, opts = {}) { opts.user ??= currentUser(); opts.appEvents = AppEvents.create(); - const store = getOwner(this).lookup("service:store"); + const store = getOwner(context).lookup("service:store"); const composer = store.createRecord("composer", opts); - if (opts.category) { - composer.set("category", opts.category); - } context.set("composer", composer); } @@ -205,7 +202,7 @@ module( activity_pub_default_visibility: "public", }); setComposer(this, { - category: this.category, + categoryId: this.category.id, }); await render(composerTemplate); diff --git a/test/javascripts/fixtures/category-followers-fixtures.js b/test/javascripts/fixtures/category-followers-fixtures.js new file mode 100644 index 00000000..c0307cb5 --- /dev/null +++ b/test/javascripts/fixtures/category-followers-fixtures.js @@ -0,0 +1,34 @@ +export default { + "/ap/category/2/followers.json": { + followers: [ + { + name: "Angus", + username: "angus_ap", + local: true, + domain: "test.local", + url: "https://test.local/u/angus_ap", + followed_at: "2013-02-08T23:14:40.018Z", + icon_url: "/images/avatar.png", + user: { + username: "angus_local", + }, + }, + { + name: "Bob", + username: "bob_ap", + local: false, + domain: "test.remote", + url: "https://test.remote/u/bob_ap", + followed_at: "2014-02-08T23:14:40.018Z", + icon_url: "/images/avatar.png", + user: { + username: "bob_local", + }, + }, + ], + meta: { + total: 2, + load_more_url: "/ap/category/2/followers.json?page=1", + }, + }, +}; From 644875af99105266bad1a0b691e6f74f023e5849 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Wed, 1 Nov 2023 11:40:31 +0800 Subject: [PATCH 15/71] Add follow acceptance tests --- .../components/activity-pub-follow-domain.hbs | 2 + .../components/activity-pub-follow-domain.js | 2 +- .../acceptance/activity-pub-discovery-test.js | 17 ++++ .../activity-pub-follow-domain-test.js | 84 +++++++++++++++++ .../javascripts/fixtures/mastodon-fixtures.js | 93 +++++++++++++++++++ 5 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 test/javascripts/components/activity-pub-follow-domain-test.js create mode 100644 test/javascripts/fixtures/mastodon-fixtures.js diff --git a/assets/javascripts/discourse/components/activity-pub-follow-domain.hbs b/assets/javascripts/discourse/components/activity-pub-follow-domain.hbs index 28f47dc6..b17ba425 100644 --- a/assets/javascripts/discourse/components/activity-pub-follow-domain.hbs +++ b/assets/javascripts/discourse/components/activity-pub-follow-domain.hbs @@ -2,11 +2,13 @@
`; + + test("with a non domain input", async function (assert) { + let domain = "notADomain"; + + await render(template); + await fillIn("#activity_pub_follow_domain_input", domain); + await click("#activity_pub_follow_domain_button"); + + assert.strictEqual( + query(".activity-pub-follow-domain-footer.error").textContent.trim(), + I18n.t("discourse_activity_pub.follow.domain.invalid"), + "displays an invalid message" + ); + }); + + test("with a non activitypub domain", async function (assert) { + let domain = "google.com"; + + pretender.get(`https://${domain}/${mastodonAboutPath}`, () => { + return response(404, "not found"); + }); + + await render(template); + await fillIn("#activity_pub_follow_domain_input", domain); + await click("#activity_pub_follow_domain_button"); + + assert.strictEqual( + query(".activity-pub-follow-domain-footer.error").textContent.trim(), + I18n.t("discourse_activity_pub.follow.domain.invalid"), + "displays an invalid message" + ); + }); + + test("with an activitypub domain", async function (assert) { + let domain = "mastodon.social"; + + pretender.get(`https://${domain}/${mastodonAboutPath}`, () => { + return response(Mastodon[`/${mastodonAboutPath}`]); + }); + + const openStub = sinon.stub(window, "open").returns(null); + + await render(template); + await fillIn("#activity_pub_follow_domain_input", domain); + await click("#activity_pub_follow_domain_button"); + + const url = `https://${domain}/authorize_interaction?uri=${encodeURIComponent( + "announcements@forum.local" + )}`; + assert.strictEqual( + openStub.calledWith(url, "_blank"), + true, + "it loads the mastodon authorize interaction route in a new tab" + ); + }); + } +); diff --git a/test/javascripts/fixtures/mastodon-fixtures.js b/test/javascripts/fixtures/mastodon-fixtures.js new file mode 100644 index 00000000..46e8b6d2 --- /dev/null +++ b/test/javascripts/fixtures/mastodon-fixtures.js @@ -0,0 +1,93 @@ +export default { + "/api/v2/instance": { + domain: "mastodon.social", + title: "Mastodon", + version: "4.2.1", + source_url: "https://github.com/mastodon/mastodon", + description: + "The original server operated by the Mastodon gGmbH non-profit", + usage: { + users: { + active_month: 277875, + }, + }, + thumbnail: { + url: + "https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png", + blurhash: "UeKUpFxuo~R%0nW;WCnhF6RjaJt757oJodS$", + versions: { + "@1x": + "https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png", + "@2x": + "https://files.mastodon.social/site_uploads/files/000/000/001/@2x/57c12f441d083cde.png", + }, + }, + languages: ["en"], + configuration: { + urls: { + streaming: "wss://streaming.mastodon.social", + status: "https://status.mastodon.social", + }, + accounts: { + max_featured_tags: 10, + }, + statuses: { + max_characters: 500, + max_media_attachments: 4, + characters_reserved_per_url: 23, + }, + media_attachments: { + supported_mime_types: [ + "image/jpeg", + "image/png", + "image/gif", + "image/heic", + "image/heif", + "image/webp", + "image/avif", + "video/webm", + "video/mp4", + "video/quicktime", + "video/ogg", + "audio/wave", + "audio/wav", + "audio/x-wav", + "audio/x-pn-wave", + "audio/vnd.wave", + "audio/ogg", + "audio/vorbis", + "audio/mpeg", + "audio/mp3", + "audio/webm", + "audio/flac", + "audio/aac", + "audio/m4a", + "audio/x-m4a", + "audio/mp4", + "audio/3gpp", + "video/x-ms-asf", + ], + image_size_limit: 16777216, + image_matrix_limit: 33177600, + video_size_limit: 103809024, + video_frame_rate_limit: 120, + video_matrix_limit: 8294400, + }, + polls: { + max_options: 4, + max_characters_per_option: 50, + min_expiration: 300, + max_expiration: 2629746, + }, + translation: { + enabled: true, + }, + }, + registrations: { + enabled: true, + approval_required: false, + message: null, + url: null, + }, + }, +}; From 6c1377bc10d3e7b6928afbe014fa3ddb25186c2b Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Thu, 2 Nov 2023 10:19:24 +0800 Subject: [PATCH 16/71] Remove ember decorators from visibility dropdown --- .../components/activity-pub-visibility-dropdown.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/assets/javascripts/discourse/components/activity-pub-visibility-dropdown.js b/assets/javascripts/discourse/components/activity-pub-visibility-dropdown.js index 80d1f08f..939100ba 100644 --- a/assets/javascripts/discourse/components/activity-pub-visibility-dropdown.js +++ b/assets/javascripts/discourse/components/activity-pub-visibility-dropdown.js @@ -1,7 +1,6 @@ import ComboBoxComponent from "select-kit/components/combo-box"; import I18n from "I18n"; import { computed } from "@ember/object"; -import { observes, on } from "@ember-decorators/object"; import { equal } from "@ember/object/computed"; import { scheduleOnce } from "@ember/runloop"; @@ -24,9 +23,9 @@ export default ComboBoxComponent.extend({ ]; }), - @on("didReceiveAttrs") - @observes("fullTopicPublication") - handleFullTopicPublication() { + didReceiveAttrs() { + this._super(...arguments); + if (this.fullTopicPublication) { this.set("value", "public"); } From d17ce615b97d517ac6cd0642b91af4db9f5889ca Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Thu, 2 Nov 2023 10:47:18 +0800 Subject: [PATCH 17/71] Update category nav text --- .../discourse/components/activity-pub-nav-item.hbs | 4 ++-- config/locales/client.en.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/assets/javascripts/discourse/components/activity-pub-nav-item.hbs b/assets/javascripts/discourse/components/activity-pub-nav-item.hbs index 9a5a9d6b..29ea8b00 100644 --- a/assets/javascripts/discourse/components/activity-pub-nav-item.hbs +++ b/assets/javascripts/discourse/components/activity-pub-nav-item.hbs @@ -1,8 +1,8 @@ {{d-icon "discourse-activity-pub"}} - {{i18n 'discourse_activity_pub.followers.label'}} + {{i18n 'discourse_activity_pub.category_nav.label'}} \ No newline at end of file diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index de002d00..af0e0c85 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -35,6 +35,9 @@ en: label: active: Active not_active: Not Active + category_nav: + label: Federation + description: Federation for %{category_name} discovery: description: Follow %{category_name} on services that support ActivityPub using handle: @@ -51,9 +54,6 @@ en: description: domain of your Mastodon account verifying: verifying the domain... invalid: not a valid Mastodon domain - followers: - label: Followers - description: People following this category via ActivityPub. follower: actor: Actor user: User From 11df0bb645e1a18ef77a86d874dc12b8ae3477fb Mon Sep 17 00:00:00 2001 From: Penar Musaraj Date: Thu, 2 Nov 2023 23:27:30 -0400 Subject: [PATCH 18/71] Update banner in category federation page Also moves some of the components to gjs. That said, it's still incomplete, tests haven't been updated. --- .../activity-pub-category-banner.gjs | 50 ++++++++++++++ .../activity-pub-category-banner.hbs | 9 --- .../activity-pub-category-banner.js | 6 -- .../components/activity-pub-discovery.hbs | 13 ---- .../components/activity-pub-discovery.js | 65 ------------------- .../components/activity-pub-follow-btn.gjs | 27 ++++++++ .../components/activity-pub-follow-btn.hbs | 6 -- .../components/activity-pub-follow-btn.js | 13 ---- .../discourse-activity-pub-handle.hbs | 3 - assets/stylesheets/common/common.scss | 56 ++++++++-------- config/locales/client.en.yml | 10 ++- 11 files changed, 111 insertions(+), 147 deletions(-) create mode 100644 assets/javascripts/discourse/components/activity-pub-category-banner.gjs delete mode 100644 assets/javascripts/discourse/components/activity-pub-category-banner.hbs delete mode 100644 assets/javascripts/discourse/components/activity-pub-category-banner.js delete mode 100644 assets/javascripts/discourse/components/activity-pub-discovery.hbs delete mode 100644 assets/javascripts/discourse/components/activity-pub-discovery.js create mode 100644 assets/javascripts/discourse/components/activity-pub-follow-btn.gjs delete mode 100644 assets/javascripts/discourse/components/activity-pub-follow-btn.hbs delete mode 100644 assets/javascripts/discourse/components/activity-pub-follow-btn.js delete mode 100644 assets/javascripts/discourse/connectors/before-create-topic-button/discourse-activity-pub-handle.hbs diff --git a/assets/javascripts/discourse/components/activity-pub-category-banner.gjs b/assets/javascripts/discourse/components/activity-pub-category-banner.gjs new file mode 100644 index 00000000..b573d66a --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-category-banner.gjs @@ -0,0 +1,50 @@ +import Component from "@glimmer/component"; +import { inject as service } from "@ember/service"; +import ActivityPubHandle from "../components/activity-pub-handle"; +import ActivityPubFollowBtn from "../components/activity-pub-follow-btn"; +import i18n from "discourse-common/helpers/i18n"; +import I18n from "discourse-i18n"; +import icon from "discourse-common/helpers/d-icon"; + +export default class ActivityPubCategoryBanner extends Component { + @service site; + + get typeDescription() { + if (this.args.category.activity_pub_default_visibility === "public") { + if (this.args.category.activity_pub_publication_type === "full_topic") { + return I18n.t("discourse_activity_pub.banner.public_full_topic"); + } else { + return I18n.t("discourse_activity_pub.banner.public_first_post"); + } + } else { + if (this.args.category.activity_pub_publication_type === "full_topic") { + return I18n.t( + "discourse_activity_pub.banner.followers_only_full_topic" + ); + } else { + return I18n.t( + "discourse_activity_pub.banner.followers_only_first_post" + ); + } + } + return ""; + } + + +} diff --git a/assets/javascripts/discourse/components/activity-pub-category-banner.hbs b/assets/javascripts/discourse/components/activity-pub-category-banner.hbs deleted file mode 100644 index e5c9f988..00000000 --- a/assets/javascripts/discourse/components/activity-pub-category-banner.hbs +++ /dev/null @@ -1,9 +0,0 @@ -
- {{#if @category}} - - {{#unless this.site.mobileView}} - - {{/unless}} - - {{/if}} -
\ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-category-banner.js b/assets/javascripts/discourse/components/activity-pub-category-banner.js deleted file mode 100644 index bad9a0c9..00000000 --- a/assets/javascripts/discourse/components/activity-pub-category-banner.js +++ /dev/null @@ -1,6 +0,0 @@ -import Component from "@glimmer/component"; -import { inject as service } from "@ember/service"; - -export default class ActivityPubCategoryBanner extends Component { - @service site; -} diff --git a/assets/javascripts/discourse/components/activity-pub-discovery.hbs b/assets/javascripts/discourse/components/activity-pub-discovery.hbs deleted file mode 100644 index 43af1562..00000000 --- a/assets/javascripts/discourse/components/activity-pub-discovery.hbs +++ /dev/null @@ -1,13 +0,0 @@ -
- - {{#if showDropdown}} -
- {{i18n "discourse_activity_pub.discovery.description" category_name=@category.name}} - -
- {{/if}} -
\ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-discovery.js b/assets/javascripts/discourse/components/activity-pub-discovery.js deleted file mode 100644 index ea62597e..00000000 --- a/assets/javascripts/discourse/components/activity-pub-discovery.js +++ /dev/null @@ -1,65 +0,0 @@ -import Component from "@glimmer/component"; -import { action } from "@ember/object"; -import { createPopper } from "@popperjs/core"; -import { tracked } from "@glimmer/tracking"; -import { bind } from "discourse-common/utils/decorators"; -import { scheduleOnce } from "@ember/runloop"; - -export default class ActivityPubDiscovery extends Component { - @tracked showDropdown = false; - - createDropdown() { - document.addEventListener("click", this.handleOutsideClick); - - const dropdown = document.querySelector(".activity-pub-discovery-dropdown"); - if (!dropdown) { - return; - } - const container = document.querySelector(".activity-pub-discovery"); - - this._popper = createPopper(container, dropdown, { - strategy: "absolute", - placement: "bottom-start", - modifiers: [ - { - name: "preventOverflow", - }, - { - name: "offset", - options: { - offset: [0, 5], - }, - }, - ], - }); - } - - willDestroy() { - this.onClose(); - } - - @bind - handleOutsideClick(event) { - const dropdown = document.querySelector(".activity-pub-discovery-dropdown"); - if (dropdown && !dropdown.contains(event.target)) { - this.onClose(event); - } - } - - @action - onClose(event) { - this.showDropdown = false; - event?.stopPropagation(); - document.removeEventListener("click", this.handleOutsideClick); - this._popper = null; - } - - @action - toggleDropdown() { - this.showDropdown = !this.showDropdown; - - if (this.showDropdown) { - scheduleOnce("afterRender", this, this.createDropdown); - } - } -} diff --git a/assets/javascripts/discourse/components/activity-pub-follow-btn.gjs b/assets/javascripts/discourse/components/activity-pub-follow-btn.gjs new file mode 100644 index 00000000..f5f564f5 --- /dev/null +++ b/assets/javascripts/discourse/components/activity-pub-follow-btn.gjs @@ -0,0 +1,27 @@ +import Component from "@glimmer/component"; +import { action } from "@ember/object"; +import { inject as service } from "@ember/service"; +import ActivityPubFollowModal from "../components/modal/activity-pub-follow"; +import i18n from "discourse-common/helpers/i18n"; +import DButton from "discourse/components/d-button"; + +export default class ActivityPubFollowBtn extends Component { + @service modal; + + @action + showModal() { + this.modal.show(ActivityPubFollowModal, { model: this.args.category }); + } + + +} diff --git a/assets/javascripts/discourse/components/activity-pub-follow-btn.hbs b/assets/javascripts/discourse/components/activity-pub-follow-btn.hbs deleted file mode 100644 index bc41af15..00000000 --- a/assets/javascripts/discourse/components/activity-pub-follow-btn.hbs +++ /dev/null @@ -1,6 +0,0 @@ - \ No newline at end of file diff --git a/assets/javascripts/discourse/components/activity-pub-follow-btn.js b/assets/javascripts/discourse/components/activity-pub-follow-btn.js deleted file mode 100644 index 203784f9..00000000 --- a/assets/javascripts/discourse/components/activity-pub-follow-btn.js +++ /dev/null @@ -1,13 +0,0 @@ -import Component from "@glimmer/component"; -import { action } from "@ember/object"; -import { inject as service } from "@ember/service"; -import ActivityPubFollowModal from "../components/modal/activity-pub-follow"; - -export default class ActivityPubFollowBtn extends Component { - @service modal; - - @action - showModal() { - this.modal.show(ActivityPubFollowModal, { model: this.args.category }); - } -} diff --git a/assets/javascripts/discourse/connectors/before-create-topic-button/discourse-activity-pub-handle.hbs b/assets/javascripts/discourse/connectors/before-create-topic-button/discourse-activity-pub-handle.hbs deleted file mode 100644 index 35dad939..00000000 --- a/assets/javascripts/discourse/connectors/before-create-topic-button/discourse-activity-pub-handle.hbs +++ /dev/null @@ -1,3 +0,0 @@ -{{#if this.category.activity_pub_show_handle}} - -{{/if}} \ No newline at end of file diff --git a/assets/stylesheets/common/common.scss b/assets/stylesheets/common/common.scss index c3dfb759..8ee601e1 100644 --- a/assets/stylesheets/common/common.scss +++ b/assets/stylesheets/common/common.scss @@ -217,43 +217,41 @@ body.user-preferences-activity-pub-page { } .activity-pub-category-banner { - display: flex; - align-items: center; + .desktop-view & { + display: flex; + align-items: center; + } width: 100%; background: var(--primary-very-low); + padding: 0.5em; + box-sizing: border-box; - .activity-pub-status { - padding: 6px 8px; - } - - .activity-pub-statistics { - flex: 1; - display: inline-flex; + .activity-pub-category-banner-left { + padding-right: 1em; + display: flex; align-items: center; - gap: 1.5em; - height: 100%; - padding: 0 1.5em; - - @media (max-width: 700px) { - display: none; + .desktop-view & { + max-width: 55%; + } + .d-icon { + color: var(--success); + width: 24px; + height: 24px; + margin: 0.5em; + margin-right: 0.75em; + .mobile-view & { + display: none; + } } } - .activity-pub-follow-btn { + .activity-pub-category-banner-right { margin-left: auto; - } - - button { - min-height: 35px; - } -} - -.activity-pub-follow-btn { - .activity-pub-status { - @include btn; - - .label { - margin-left: unset; + margin-top: 0.5em; + max-width: 45%; + .activity-pub-handle { + margin-bottom: 0.5em; + margin-right: 0.5em; } } } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index af0e0c85..7f506c0d 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -38,11 +38,15 @@ en: category_nav: label: Federation description: Federation for %{category_name} - discovery: - description: Follow %{category_name} on services that support ActivityPub using + banner: + intro: "This category's topics can be followed by users on ActivityPub social media like Mastodon. " + public_full_topic: Each post of each topic will be published publicly. + public_first_post: The first post of each topic will be published publicly. + followers_only_full_topic: Each post of each topic is published to ActivityPub followers only. + followers_only_first_post: The first post of each topic is published to ActivityPub followers only. handle: label: Other - description: search for this handle on a service that supports ActivityPub + description: Search for this handle on a service that supports ActivityPub. follow: label: Follow title: Follow %{name} via ActivityPub From 8e926e0a353b959cde8476052affb145ef683c95 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Thu, 9 Nov 2023 16:42:53 +0000 Subject: [PATCH 19/71] Complete banner changes and tests --- .../activity-pub-category-banner.gjs | 51 ++++++------ .../components/activity-pub-nav-item.hbs | 22 +++-- .../components/activity-pub-nav-item.js | 36 ++++++++- .../activity-pub-category-navigation.hbs | 4 +- assets/stylesheets/common/common.scss | 72 ++++++----------- config/locales/client.en.yml | 14 ++-- .../acceptance/activity-pub-discovery-test.js | 81 +++++++++---------- 7 files changed, 140 insertions(+), 140 deletions(-) diff --git a/assets/javascripts/discourse/components/activity-pub-category-banner.gjs b/assets/javascripts/discourse/components/activity-pub-category-banner.gjs index b573d66a..8113fce0 100644 --- a/assets/javascripts/discourse/components/activity-pub-category-banner.gjs +++ b/assets/javascripts/discourse/components/activity-pub-category-banner.gjs @@ -5,43 +5,40 @@ import ActivityPubFollowBtn from "../components/activity-pub-follow-btn"; import i18n from "discourse-common/helpers/i18n"; import I18n from "discourse-i18n"; import icon from "discourse-common/helpers/d-icon"; +import DTooltip from "float-kit/components/d-tooltip"; export default class ActivityPubCategoryBanner extends Component { @service site; - get typeDescription() { - if (this.args.category.activity_pub_default_visibility === "public") { - if (this.args.category.activity_pub_publication_type === "full_topic") { - return I18n.t("discourse_activity_pub.banner.public_full_topic"); - } else { - return I18n.t("discourse_activity_pub.banner.public_first_post"); - } - } else { - if (this.args.category.activity_pub_publication_type === "full_topic") { - return I18n.t( - "discourse_activity_pub.banner.followers_only_full_topic" - ); - } else { - return I18n.t( - "discourse_activity_pub.banner.followers_only_first_post" - ); - } - } - return ""; + get bannerDescription() { + const visibility = this.args.category.activity_pub_default_visibility; + const publicationType = this.args.category.activity_pub_publication_type; + return I18n.t(`discourse_activity_pub.banner.${visibility}_${publicationType}`); + } + + get bannerText() { + const key = this.site.mobileView ? 'mobile_text' : 'text'; + return I18n.t(`discourse_activity_pub.banner.${key}`, { + category_name: this.args.category.name + }); }