card-mod-follow 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,70 @@
1
+ # used by +:followers overwritten in type/set.rb and type/cardtype.rb
2
+ def followed?
3
+ followed_by? Auth.current_id
4
+ end
5
+
6
+ # for sets and cardtypes it doesn't check whether the users is following the
7
+ # card itself instead it checks whether he is following the complete set
8
+ def followed_by? user_id
9
+ follow_rule_applies?(user_id) || left&.followed_by_as_field?(self, user_id)
10
+ end
11
+
12
+ def followed_by_as_field? field, user_id
13
+ followed_field?(field) && followed_by?(user_id)
14
+ end
15
+
16
+ # returns true if according to the follow_field_rule followers of self also
17
+ # follow changes of field_card
18
+ def followed_field? field_card
19
+ return unless (follow_field_rule = rule_card(:follow_fields))
20
+
21
+ follow_field_rule.item_names(context: self).find do |item|
22
+ case item.to_name.key
23
+ when field_card.key then true
24
+ when :nests.cardname.key then nested_card?(field_card)
25
+ end
26
+ end
27
+ end
28
+
29
+ def nested_card? card
30
+ nestee_ids.include? card.id
31
+ end
32
+
33
+ ## the following methods all handle _explicit_ (direct) follow rules (not fields)
34
+
35
+ def follow_rule_applies? follower_id
36
+ !follow_rule_option(follower_id).nil?
37
+ end
38
+
39
+ def follow_rule_option follower_id
40
+ all_follow_rule_options(follower_id).find do |option|
41
+ follow_rule_option_applies? follower_id, option
42
+ end
43
+ end
44
+
45
+ def all_follow_rule_options follower_id
46
+ follow_rule = preference :follow, follower_id
47
+ return [] unless follow_rule.present?
48
+
49
+ follow_rule.split("\n")
50
+ end
51
+
52
+ def follow_rule_option_applies? follower_id, option
53
+ option_code = option.to_name.code
54
+ candidate_ids = follower_candidate_ids_for_option option_code
55
+ follow_rule_option_applies_to_candidates? follower_id, option_code, candidate_ids
56
+ end
57
+
58
+ def follow_rule_option_applies_to_candidates? follower_id, option_code, candidate_ids
59
+ if (test = FollowOption.test[option_code])
60
+ test.call follower_id, candidate_ids
61
+ else
62
+ candidate_ids.include? follower_id
63
+ end
64
+ end
65
+
66
+ def follower_candidate_ids_for_option option_code
67
+ return [] unless (block = FollowOption.follower_candidate_ids[option_code])
68
+
69
+ block.call self
70
+ end
@@ -0,0 +1,121 @@
1
+ FOLLOWER_IDS_CACHE_KEY = "FOLLOWER_IDS".freeze
2
+
3
+ card_accessor :followers
4
+
5
+ event :cache_expired_for_type_change, :store, on: :update, changed: %i[type_id name] do
6
+ act_card&.schedule_preference_expiration
7
+ # FIXME: expire (also?) after save
8
+ Card.follow_caches_expired
9
+ end
10
+
11
+ def schedule_preference_expiration
12
+ @expire_preferences_scheduled = true
13
+ end
14
+
15
+ def expire_preferences?
16
+ @expire_preferences_scheduled
17
+ end
18
+
19
+ event :expire_preferences_cache, :finalize, when: :expire_preferences? do
20
+ Card::Rule.clear_preference_cache
21
+ end
22
+
23
+ # follow cache methods on Card class
24
+ module ClassMethods
25
+ def follow_caches_expired
26
+ Card.clear_follower_ids_cache
27
+ Card::Rule.clear_preference_cache
28
+ end
29
+
30
+ def follower_ids_cache
31
+ Card.cache.read(FOLLOWER_IDS_CACHE_KEY) || {}
32
+ end
33
+
34
+ def write_follower_ids_cache hash
35
+ Card.cache.write FOLLOWER_IDS_CACHE_KEY, hash
36
+ end
37
+
38
+ def clear_follower_ids_cache
39
+ Card.cache.write FOLLOWER_IDS_CACHE_KEY, nil
40
+ end
41
+ end
42
+
43
+ def write_follower_ids_cache user_ids
44
+ hash = Card.follower_ids_cache
45
+ hash[id] = user_ids
46
+ Card.write_follower_ids_cache hash
47
+ end
48
+
49
+ def read_follower_ids_cache
50
+ Card.follower_ids_cache[id]
51
+ end
52
+
53
+ def follower_names
54
+ followers.map(&:name)
55
+ end
56
+
57
+ def followers
58
+ follower_ids.map do |id|
59
+ Card.fetch(id)
60
+ end
61
+ end
62
+
63
+ def follower_ids
64
+ @follower_ids = read_follower_ids_cache || begin
65
+ result = direct_follower_ids + indirect_follower_ids
66
+ write_follower_ids_cache result
67
+ result
68
+ end
69
+ end
70
+
71
+ def followers_count
72
+ follower_ids.size
73
+ end
74
+
75
+ def indirect_follower_ids
76
+ result = ::Set.new
77
+ left_card = left
78
+ while left_card
79
+ result += left_card.direct_follower_ids if left_card.followed_field? self
80
+ left_card = left_card.left
81
+ end
82
+ result
83
+ end
84
+
85
+ # all users (cards) that "directly" follow this card
86
+ # "direct" means there is a follow rule that applies explicitly to this card.
87
+ # one can also "indirectly" follow cards by following parent cards or other
88
+ # cards that nest this one.
89
+ def direct_followers
90
+ direct_follower_ids.map do |id|
91
+ Card.fetch(id)
92
+ end
93
+ end
94
+
95
+ def direct_follower_ids &block
96
+ ids = ::Set.new
97
+ set_names.each do |set_name|
98
+ direct_follower_ids_for_set setcard_from_name(set_name), ids, &block
99
+ end
100
+ ids
101
+ end
102
+
103
+ def setcard_from_name set_name
104
+ Card.fetch set_name, new: { type_id: SetID }
105
+ end
106
+
107
+ def direct_follower_ids_for_set set_card, ids
108
+ set_card.all_user_ids_with_rule_for(:follow).each do |user_id|
109
+ next if ids.include?(user_id) || !(option = follow_rule_option user_id)
110
+
111
+ yield user_id, set_card, option if block_given?
112
+ ids << user_id
113
+ end
114
+ end
115
+
116
+ def each_direct_follower_id_with_reason
117
+ direct_follower_ids do |user_id, set_card, follow_option|
118
+ reason = follow_option.gsub(/[\[\]]/, "")
119
+ yield user_id, set_card: set_card, option: reason
120
+ end
121
+ end
@@ -0,0 +1,11 @@
1
+ #! no set module
2
+
3
+ class StartFollowLink < FollowLink
4
+ def initialize format
5
+ @rule_content = "*always"
6
+ @link_text = "follow"
7
+ @action = "send"
8
+ @css_class = ""
9
+ super
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ #! no set module
2
+
3
+ class StopFollowLink < FollowLink
4
+ def initialize format
5
+ @rule_content = "*never"
6
+ @link_text = "following"
7
+ @hover_text = "unfollow"
8
+ @action = "stop sending"
9
+ @css_class = "btn-item-delete"
10
+ super
11
+ end
12
+ end
@@ -0,0 +1,81 @@
1
+ attr_accessor :follower_stash
2
+ mattr_accessor :force_notifications
3
+
4
+ event :silence_notifications, :initialize, when: :silence_notifications? do
5
+ @silent_change = true
6
+ end
7
+
8
+ def silence_notifications?
9
+ !(Card::Env[:controller] || force_notifications)
10
+ end
11
+
12
+ event :notify_followers_after_save,
13
+ :integrate_with_delay, on: :save, when: :notable_change? do
14
+ notify_followers
15
+ end
16
+
17
+ # in the delete case we have to calculate the follower_stash beforehand
18
+ # but we can't pass the follower_stash through the ActiveJob queue.
19
+ # We have to deal with the notifications in the integrate phase instead of the
20
+ # integrate_with_delay phase
21
+ event :stash_followers, :store, on: :delete, when: :notable_change? do
22
+ act_card.follower_stash ||= FollowerStash.new
23
+ act_card.follower_stash.check_card self
24
+ end
25
+
26
+ event :notify_followers_after_delete, :integrate, on: :delete, when: :notable_change? do
27
+ notify_followers
28
+ end
29
+
30
+ def notify_followers
31
+ return unless (act = Card::Director.act)
32
+
33
+ act.reload
34
+ notify_followers_of act
35
+ end
36
+
37
+ def notable_change?
38
+ !silent_change? && current_act_card? &&
39
+ (Card::Auth.current_id != WagnBotID) && followable?
40
+ end
41
+
42
+ def silent_change?
43
+ silent_change
44
+ end
45
+
46
+ private
47
+
48
+ def notify_followers_of act
49
+ act_followers(act).each_follower_with_reason do |follower, reason|
50
+ next if !follower.account || (follower == act.actor)
51
+
52
+ notify_follower follower, act, reason
53
+ end
54
+ end
55
+
56
+ def notify_follower follower, act, reason
57
+ follower.account.send_change_notice act, reason[:set_card].name, reason[:option]
58
+ end
59
+
60
+ def act_followers act
61
+ @follower_stash ||= FollowerStash.new
62
+ act.actions(false).each do |a|
63
+ next if !a.card || a.card.silent_change?
64
+
65
+ @follower_stash.check_card a.card
66
+ end
67
+ @follower_stash
68
+ end
69
+
70
+ def silent_change
71
+ @silent_change || @supercard&.silent_change
72
+ end
73
+
74
+ def current_act_card?
75
+ return false unless act_card
76
+
77
+ act_card.id.nil? || act_card.id == id
78
+ # FIXME: currently card_id is nil for deleted acts (at least
79
+ # in the store phase when it's tested). The nil test was needed
80
+ # to make this work.
81
+ end
@@ -0,0 +1,127 @@
1
+ format do
2
+ view :list_of_changes, denial: :blank, cache: :never do
3
+ action = notification_action voo.action_id
4
+ relevant_fields(action).map do |type|
5
+ edit_info_for(type, action)
6
+ end.compact.join
7
+ end
8
+
9
+ view :subedits, perms: :none, cache: :never do
10
+ return unless notification_act
11
+
12
+ wrap_subedits do
13
+ notification_act.actions_affecting(card).map do |action|
14
+ next if action.card_id == card.id
15
+
16
+ action.card.format(format: @format).render_subedit_notice action_id: action.id
17
+ end
18
+ end
19
+ end
20
+
21
+ view :subedit_notice, cache: :never do
22
+ action = notification_action voo.action_id
23
+ wrap_subedit_item do
24
+ %(#{name_before_action action} #{action.action_type}d\n) +
25
+ render_list_of_changes(action_id: action.id)
26
+ end
27
+ end
28
+
29
+ view :followed, perms: :none, compact: true do
30
+ if (set_card = followed_set_card) && (option_card = follow_option_card)
31
+ option_card.description set_card
32
+ else
33
+ "*followed set of cards*"
34
+ end
35
+ end
36
+
37
+ view :follower, perms: :none, compact: true do
38
+ active_notice(:follower) || "follower"
39
+ end
40
+
41
+ view :last_action_verb, cache: :never do
42
+ "#{notification_act&.main_action&.action_type || 'edite'}d"
43
+ end
44
+
45
+ view :unfollow_url, perms: :none, compact: true, cache: :never do
46
+ return "" unless (rule_name = live_follow_rule_name)
47
+
48
+ card_url path(mark: "#{active_notice(:follower)}+#{:follow.cardname}",
49
+ action: :update,
50
+ card: { subcards: { rule_name => Card[:never].name } })
51
+ end
52
+
53
+ def relevant_fields action
54
+ case action.action_type
55
+ when :create then %i[cardtype content]
56
+ when :update then %i[name cardtype content]
57
+ when :delete then %i[content]
58
+ end
59
+ end
60
+
61
+ def name_before_action action
62
+ (action.value(:name) && action.previous_value(:name)) || card.name
63
+ end
64
+
65
+ def followed_set_card
66
+ (set_name = active_notice(:followed_set)) && Card.fetch(set_name)
67
+ end
68
+
69
+ def follow_option_card
70
+ return unless (option_name = active_notice(:follow_option))
71
+
72
+ Card.fetch option_name
73
+ end
74
+
75
+ def active_notice key
76
+ @active_notice ||= inherit :active_notice
77
+ return unless @active_notice
78
+
79
+ @active_notice[key]
80
+ end
81
+
82
+ def live_follow_rule_name
83
+ return unless (set_card = followed_set_card) && (follower = active_notice(:follower))
84
+
85
+ set_card.follow_rule_name follower
86
+ end
87
+
88
+ def edit_info_for field, action
89
+ return nil unless (value = action.value field)
90
+ value = action.previous_value if action.action_type == :delete
91
+ wrap_list_item " #{notification_action_label action} #{field}: #{value}"
92
+ end
93
+
94
+ def notification_action_label action
95
+ case action.action_type
96
+ when :update then "new"
97
+ when :delete then "deleted"
98
+ end
99
+ end
100
+
101
+ def wrap_subedits
102
+ subedits = yield.compact.join
103
+ return "" if subedits.blank?
104
+
105
+ "\nThis update included the following changes:\n#{wrap_list subedits}"
106
+ end
107
+
108
+ def wrap_list list
109
+ "\n#{list}\n"
110
+ end
111
+
112
+ def wrap_list_item item
113
+ "#{item}\n"
114
+ end
115
+
116
+ def wrap_subedit_item
117
+ "\n#{yield}\n"
118
+ end
119
+
120
+ def notification_act act=nil
121
+ @notification_act ||= act || card.acts.last
122
+ end
123
+
124
+ def notification_action action_id
125
+ action_id ? Action.fetch(action_id) : card.last_action
126
+ end
127
+ end
@@ -0,0 +1,17 @@
1
+ format :html do
2
+ # view :last_action, perms: :none, cache: :never do
3
+ # _render_last_action_verb
4
+ # end
5
+
6
+ def wrap_list list
7
+ "<ul>#{list}</ul>\n"
8
+ end
9
+
10
+ def wrap_list_item item
11
+ "<li>#{item}</li>\n"
12
+ end
13
+
14
+ def wrap_subedit_item
15
+ "<li>#{yield}</li>\n"
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ def send_change_notice act, followed_set, follow_option
2
+ return unless email.present? && changes_visible?(act)
3
+
4
+ notify_of_act act do
5
+ { follower: left.name, followed_set: followed_set, follow_option: follow_option }
6
+ end
7
+ end
8
+
9
+ def notify_of_act act
10
+ Auth.as(left.id) do
11
+ Card[:follower_notification_email].deliver(
12
+ act.card, { to: email }, auth: left, active_notice: yield
13
+ )
14
+ end
15
+ end