cubism 0.1.0.pre3 → 0.1.0.pre4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 552b9be661b93fdddc9ded1d3b95316ef341fc750ea9950edf6d3d5e84f7ae46
4
- data.tar.gz: 58e4f47f505d94f1f23294699208746cb344ea36b1858aef4596a06396f71838
3
+ metadata.gz: af6a4e98fdf0f80e86235cd109e063af6535651ab112bb6b50329eda64eae2ef
4
+ data.tar.gz: 85dd646220a2d3b040ac409489e52462f969fbd935d66abe234f1eeebf51828c
5
5
  SHA512:
6
- metadata.gz: d1c8117ba4c9c31db45a293678ac38f8427c392cde8e7ac7cc2c46eb5a7801195917ac78db78e2e0a528d6059fcc15c6db5df415edb0d671a4d12b0bc3230935
7
- data.tar.gz: 682f88933c469912f8f36b3188942d4a88a4aa78229194c5936116edc285e846dc1a3ad3db829efb1b3654343f0ea9168751899c299d1d2773cd4efc7e3c01d3
6
+ metadata.gz: c8eb831972225222b251825d9accccbebdb09aaf9298c963a0355501be3a94ecd6d26be1af60dd78bc74ee279da83cf24aa612a73cb90a33b07ade5e64e00619
7
+ data.tar.gz: 18e35469a81269c4b23e17fb141543594fb44d0195c8e48650d7c895c82ed0c2dfa4ad65ddab6a06a194b2ddc8c252de64c4fdb5403b940acb925d72d044f758
data/README.md CHANGED
@@ -1,4 +1,7 @@
1
1
  # Cubism
2
+ <!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
3
+ [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)
4
+ <!-- ALL-CONTRIBUTORS-BADGE:END -->
2
5
  [![Twitter follow](https://img.shields.io/twitter/follow/julian_rubisch?style=social)](https://twitter.com/julian_rubisch)
3
6
 
4
7
  Lightweight Resource-Based Presence Solution with CableReady
@@ -8,7 +11,8 @@ Lightweight Resource-Based Presence Solution with CableReady
8
11
  - [Table of Contents](#table-of-contents)
9
12
  - [Usage](#usage)
10
13
  - [Installation](#installation)
11
- - [Manual Installation](#manual-installation)
14
+ - [API](#api)
15
+ - [Gotchas](#gotchas)
12
16
  - [Contributing](#contributing)
13
17
  - [License](#license)
14
18
  - [Contributors](#contributors)
@@ -16,12 +20,11 @@ Lightweight Resource-Based Presence Solution with CableReady
16
20
  ## Usage
17
21
 
18
22
  ### Prepare your User Model
19
- In your app's `User` model, include `Cubism::User` and set up a safelist of exposed attributes:
23
+ In your app's `User` model, include `Cubism::User`:
20
24
 
21
25
  ```rb
22
26
  class User < ApplicationRecord
23
27
  include Cubism::User
24
- self.cubicle_attributes = %i[email id]
25
28
 
26
29
  # ...
27
30
  end
@@ -33,6 +36,8 @@ In the models you'd like to track presence for, include the `Cubism::Presence` c
33
36
  ```rb
34
37
  class Project < ApplicationRecord
35
38
  include Cubism::Presence
39
+
40
+ # ...
36
41
  end
37
42
  ```
38
43
 
@@ -41,18 +46,14 @@ end
41
46
  Using the `cubicle_for` helper, you can set up a presence indicator. It will
42
47
 
43
48
  1. subscribe to the respective resource, and
44
- 2. accept a "template" using the attributes safelisted above. Elements marked with `data-cubicle-attribute=` will have their `innerHTML` replaced by Cubism.
49
+ 2. render a block which is passed the list of present `users`:
45
50
 
46
51
  ```erb
47
- <%= cubicle_for @project, current_user do %>
48
- <span class="avatar">
49
- <span data-cubicle-attribute="email"></span>
50
- </span>
52
+ <%= cubicle_for @project, current_user do |users| %>
53
+ <%= users.map(&:username).join(", ")
51
54
  <% end %>
52
55
  ```
53
56
 
54
- Note that this template will simply be repeated for every user that's in the `@project`s `present_users` set.
55
-
56
57
  ## Installation
57
58
  Add this line to your application's Gemfile:
58
59
 
@@ -86,6 +87,39 @@ import "@minthesize/cubism";
86
87
  CableReady.initialize({ consumer });
87
88
  ```
88
89
 
90
+ ## API
91
+
92
+ The `cubicle_for` helper accepts the following options as keyword arguments:
93
+
94
+ - `exclude_current_user (true|false)`: Whether or not to exclude the current user from the list of present users broadcasted to the view. Useful e.g. for "typing..." indicators.
95
+ - `appear_trigger`: a JavaScript event name (e.g. `:focus`) to use. The default is `:connect`, i.e. register a user as "appeared"/"present" when the element connects to the DOM.
96
+ - `disappear_trigger`: a JavaScript event name (e.g. `:blur`) to use. The default is `:disconnect`, i.e. remove a user form the present users list when the element disconnects from the DOM.
97
+ - `trigger_root`: a CSS selector to attach the appear/disappear events to. Defaults to the `cubicle-element` itself.
98
+ - `html_options` are passed to the TagBuilder.
99
+
100
+ ## Gotchas
101
+
102
+ ### Usage with ViewComponent
103
+
104
+ Currently there's a bug in VC resulting in the `capture` helper not working correctly (https://github.com/github/view_component/pull/974). The current workaround is to assign a slot in your component and render the presence list from outside:
105
+
106
+ ```rb
107
+ class MyComponent < ViewComponent::Base
108
+ renders_one :presence_list
109
+
110
+ # ...
111
+ end
112
+ ```
113
+
114
+ ```erb
115
+ <%= render MyComponent.new do |c| %>
116
+ <% c.presence_list do %>
117
+ <%= cubicle_for @project, current_user do |users| %>
118
+ ...
119
+ <% end %>
120
+ <% end %>
121
+ <% end %>
122
+ ```
89
123
 
90
124
  ## Contributing
91
125
 
@@ -130,3 +164,17 @@ yarn install --force
130
164
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
131
165
 
132
166
  ## Contributors
167
+
168
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
169
+ <!-- prettier-ignore-start -->
170
+ <!-- markdownlint-disable -->
171
+ <table>
172
+ <tr>
173
+ <td align="center"><a href="http://www.minthesize.com"><img src="https://avatars.githubusercontent.com/u/4352208?v=4?s=100" width="100px;" alt=""/><br /><sub><b>Julian Rubisch</b></sub></a><br /><a href="https://github.com/julianrubisch/cubism/commits?author=julianrubisch" title="Code">💻</a></td>
174
+ </tr>
175
+ </table>
176
+
177
+ <!-- markdownlint-restore -->
178
+ <!-- prettier-ignore-end -->
179
+
180
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
@@ -4,7 +4,8 @@ class Cubism::PresenceChannel < ActionCable::Channel::Base
4
4
  def subscribed
5
5
  if resource.present?
6
6
  stream_for resource
7
- resource.present_users.add(user.id)
7
+ resource.cubicle_element_ids << element_id
8
+ resource.excluded_user_id_for_element_id[element_id] = user.id if exclude_current_user?
8
9
  else
9
10
  reject
10
11
  end
@@ -13,6 +14,16 @@ class Cubism::PresenceChannel < ActionCable::Channel::Base
13
14
  def unsubscribed
14
15
  return unless resource.present?
15
16
 
17
+ resource.cubicle_element_ids.remove(element_id)
18
+ resource.excluded_user_id_for_element_id.delete(element_id)
19
+ disappear
20
+ end
21
+
22
+ def appear
23
+ resource.present_users.add(user.id)
24
+ end
25
+
26
+ def disappear
16
27
  resource.present_users.remove(user.id)
17
28
  end
18
29
 
@@ -26,4 +37,16 @@ class Cubism::PresenceChannel < ActionCable::Channel::Base
26
37
  def user
27
38
  GlobalID::Locator.locate_signed(params[:user])
28
39
  end
40
+
41
+ def exclude_current_user?
42
+ params[:exclude_current_user]
43
+ end
44
+
45
+ def element_id
46
+ params[:element_id]
47
+ end
48
+
49
+ def url
50
+ params[:url]
51
+ end
29
52
  end
@@ -4,7 +4,7 @@ class Cubism::PresenceChannel < ActionCable::Channel::Base
4
4
  def subscribed
5
5
  if resource.present?
6
6
  stream_for resource
7
- resource.present_users.add(current_user.id)
7
+ resource.present_users.add(user.id)
8
8
  else
9
9
  reject
10
10
  end
@@ -13,7 +13,7 @@ class Cubism::PresenceChannel < ActionCable::Channel::Base
13
13
  def unsubscribed
14
14
  return unless resource.present?
15
15
 
16
- resource.present_users.remove(current_user.id)
16
+ resource.present_users.remove(user.id)
17
17
  end
18
18
 
19
19
  private
@@ -22,4 +22,8 @@ class Cubism::PresenceChannel < ActionCable::Channel::Base
22
22
  locator = verified_stream_identifier(params[:identifier])
23
23
  GlobalID::Locator.locate(locator)
24
24
  end
25
+
26
+ def user
27
+ GlobalID::Locator.locate_signed(params[:user])
28
+ end
25
29
  end
@@ -1,11 +1,20 @@
1
1
  module CubismHelper
2
2
  include CableReady::StreamIdentifier
3
3
 
4
- def cubicle_for(resource, user, html_options: {}, &block)
5
- template = capture(&block)
4
+ def cubicle_for(resource, user, html_options: {}, appear_trigger: :connect, disappear_trigger: nil, trigger_root: nil, exclude_current_user: true, &block)
5
+ key = "#{block.source_location.join(":")}:#{resource.to_gid}:#{user.to_gid}"
6
+ digested_id = ActiveSupport::Digest.hexdigest(key)
6
7
 
7
- tag.cubicle_element({identifier: signed_stream_identifier(resource.to_global_id.to_s), user: user.to_sgid.to_s}) do
8
- content_tag(:template, template, {slot: "template"})
9
- end
8
+ Cubism.store[digested_id] = Cubism::BlockStoreItem.new(context: self, block: block.dup)
9
+ tag.cubicle_element(
10
+ identifier: signed_stream_identifier(resource.to_gid.to_s),
11
+ user: user.to_sgid.to_s,
12
+ "appear-trigger": appear_trigger,
13
+ "disappear-trigger": disappear_trigger,
14
+ "trigger-root": trigger_root,
15
+ id: "cubicle-#{digested_id}",
16
+ "exclude-current-user": exclude_current_user,
17
+ **html_options
18
+ )
10
19
  end
11
20
  end
@@ -1,10 +1,23 @@
1
1
  module CubismHelper
2
2
  include CableReady::StreamIdentifier
3
3
 
4
- def cubicle_for(resource, html_options: {}, &block)
4
+ def cubicle_for(resource, user, html_options: {}, appear_trigger: :connect, disappear_trigger: nil, trigger_root: nil, exclude_current_user: true, &block)
5
+ key = "#{block.source_location.join(":")}:#{resource.to_gid}:#{user.to_gid}"
6
+ verifiable_id = CableReady.signed_stream_verifier.generate(key)
7
+
8
+ Cubism.store[verifiable_id] = Cubism::BlockStoreItem.new(context: self, block: block.dup)
5
9
  template = capture(&block)
6
10
 
7
- tag.cubicle_element({identifier: signed_stream_identifier(resource.to_global_id.to_s)}) do
11
+ tag.cubicle_element(
12
+ identifier: signed_stream_identifier(resource.to_gid.to_s),
13
+ user: user.to_sgid.to_s,
14
+ "appear-trigger": appear_trigger,
15
+ "disappear-trigger": disappear_trigger,
16
+ "trigger-root": trigger_root,
17
+ id: "cubicle-#{verifiable_id}",
18
+ "exclude-current-user": exclude_current_user,
19
+ **html_options
20
+ ) do
8
21
  content_tag(:template, template, {slot: "template"})
9
22
  end
10
23
  end
@@ -2,10 +2,12 @@ module Cubism::Presence
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
5
- kredis_set :present_users, after_change: :stream_presence_later
5
+ kredis_set :present_users, after_change: :stream_presence
6
+ kredis_set :cubicle_element_ids
7
+ kredis_hash :excluded_user_id_for_element_id
6
8
  end
7
9
 
8
- def stream_presence_later
9
- Cubism::StreamPresenceJob.perform_later(resource: self)
10
+ def stream_presence
11
+ Cubism::Broadcaster.new(resource: self).broadcast
10
12
  end
11
13
  end
@@ -3,9 +3,5 @@ module Cubism::User
3
3
 
4
4
  included do
5
5
  Cubism.user_class = self
6
-
7
- class_eval do
8
- cattr_accessor :cubicle_attributes
9
- end
10
6
  end
11
7
  end
@@ -0,0 +1,11 @@
1
+ module Cubism::User
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ Cubism.user_class = self
6
+
7
+ class_eval do
8
+ cattr_accessor :cubicle_attributes
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module Cubism
2
+ class Current < ActiveSupport::CurrentAttributes
3
+ attribute :user
4
+ end
5
+ end
File without changes
@@ -0,0 +1,37 @@
1
+ require "cable_ready"
2
+
3
+ module Cubism
4
+ class Broadcaster
5
+ include CableReady::Broadcaster
6
+ include CableReady::StreamIdentifier
7
+
8
+ attr_reader :resource
9
+
10
+ def initialize(resource:)
11
+ @resource = resource
12
+ end
13
+
14
+ def broadcast
15
+ resource.cubicle_element_ids.to_a.each do |element_id|
16
+ /cubicle-(?<block_key>.+)/ =~ element_id
17
+ block = Cubism.store[block_key].block
18
+ view_context = Cubism.store[block_key].context
19
+ html = view_context.capture(users_for(resource, element_id), &block)
20
+
21
+ cable_ready[Cubism::PresenceChannel].inner_html(
22
+ selector: "cubicle-element##{element_id}[identifier='#{signed_stream_identifier(resource.to_global_id.to_s)}']",
23
+ html: html
24
+ ).broadcast_to(resource)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def users_for(resource, element_id)
31
+ users = Cubism.user_class.find(resource.present_users.members)
32
+ users.reject! { |user| user.id == resource.excluded_user_id_for_element_id[element_id].to_i }
33
+
34
+ users
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,35 @@
1
+ module Cubism
2
+ class Broadcaster
3
+ include CableReady::Broadcaster
4
+ include CableReady::StreamIdentifier
5
+
6
+ attr_reader :resource
7
+
8
+ def initialize(resource:)
9
+ @resource = resource
10
+ end
11
+
12
+ def broadcast
13
+ resource.cubicle_element_ids.to_a.each do |element_id|
14
+ /cubicle-(?<block_key>.+)/ =~ element_id
15
+ block = Cubism.store[block_key].block
16
+ view_context = Cubism.store[block_key].context
17
+ html = view_context.capture(users_for(resource, element_id), &block)
18
+
19
+ cable_ready[Cubism::PresenceChannel].inner_html(
20
+ selector: "cubicle-element##{element_id}[identifier='#{signed_stream_identifier(resource.to_global_id.to_s)}']",
21
+ html: html
22
+ ).broadcast_to(resource)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def users_for(resource, element_id)
29
+ users = Cubism.user_class.find(resource.present_users.members)
30
+ users.reject! { |user| user.id == resource.excluded_user_id_for_element_id[element_id].to_i }
31
+
32
+ users
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,27 @@
1
+ module Cubism
2
+ class CubicleBlockStore
3
+ include Singleton
4
+
5
+ def initialize
6
+ @blocks = {}
7
+ end
8
+
9
+ def [](key)
10
+ @blocks[key]
11
+ end
12
+
13
+ def []=(key, value)
14
+ mutex.synchronize do
15
+ @blocks[key] = value
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def mutex
22
+ @mutex ||= Mutex.new
23
+ end
24
+ end
25
+
26
+ BlockStoreItem = Struct.new(:context, :block, keyword_init: true)
27
+ end
File without changes
@@ -1,3 +1,3 @@
1
1
  module Cubism
2
- VERSION = "0.1.0.pre3"
2
+ VERSION = "0.1.0.pre4"
3
3
  end
@@ -1,3 +1,3 @@
1
1
  module Cubism
2
- VERSION = "0.1.0.pre2"
2
+ VERSION = "0.1.0.pre3"
3
3
  end
data/lib/cubism.rb CHANGED
@@ -2,7 +2,15 @@ require "kredis"
2
2
 
3
3
  require "cubism/version"
4
4
  require "cubism/engine"
5
+ require "cubism/broadcaster"
6
+ require "cubism/cubicle_block_store"
5
7
 
6
8
  module Cubism
9
+ extend ActiveSupport::Autoload
10
+
11
+ autoload :Broadcaster, "cubism/broadcaster"
12
+
7
13
  mattr_accessor :user_class, instance_writer: false, instance_reader: false
14
+
15
+ mattr_reader :store, instance_reader: false, default: Cubism::CubicleBlockStore.instance
8
16
  end
data/lib/cubism.rb~ CHANGED
@@ -1,3 +1,5 @@
1
+ require "kredis"
2
+
1
3
  require "cubism/version"
2
4
  require "cubism/engine"
3
5
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cubism
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0.pre3
4
+ version: 0.1.0.pre4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Julian Rubisch
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-15 00:00:00.000000000 Z
11
+ date: 2021-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -44,14 +44,28 @@ dependencies:
44
44
  requirements:
45
45
  - - '='
46
46
  - !ruby/object:Gem::Version
47
- version: 5.0.0.pre6
47
+ version: 5.0.0.pre8
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - '='
53
53
  - !ruby/object:Gem::Version
54
- version: 5.0.0.pre6
54
+ version: 5.0.0.pre8
55
+ - !ruby/object:Gem::Dependency
56
+ name: pry
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: standard
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -81,8 +95,6 @@ files:
81
95
  - app/channels/cubism/presence_channel.rb~
82
96
  - app/helpers/cubism_helper.rb
83
97
  - app/helpers/cubism_helper.rb~
84
- - app/jobs/cubism/stream_presence_job.rb
85
- - app/jobs/cubism/stream_presence_job.rb~
86
98
  - app/models/concerns/cubism/base.rb~
87
99
  - app/models/concerns/cubism/presence.rb
88
100
  - app/models/concerns/cubism/presence.rb~
@@ -90,9 +102,15 @@ files:
90
102
  - app/models/concerns/cubism/user.rb~
91
103
  - app/models/cubism/base.rb
92
104
  - app/models/cubism/base.rb~
105
+ - app/models/cubism/current.rb
106
+ - app/models/cubism/current.rb~
93
107
  - config/routes.rb
94
108
  - lib/cubism.rb
95
109
  - lib/cubism.rb~
110
+ - lib/cubism/broadcaster.rb
111
+ - lib/cubism/broadcaster.rb~
112
+ - lib/cubism/cubicle_block_store.rb
113
+ - lib/cubism/cubicle_block_store.rb~
96
114
  - lib/cubism/engine.rb
97
115
  - lib/cubism/engine.rb~
98
116
  - lib/cubism/railtie.rb~
@@ -1,15 +0,0 @@
1
- class Cubism::StreamPresenceJob < ApplicationJob
2
- include CableReady::Broadcaster
3
- include CableReady::StreamIdentifier
4
- queue_as :default
5
-
6
- def perform(resource:)
7
- cable_ready[Cubism::PresenceChannel].dispatch_event(
8
- name: "cubism:update",
9
- selector: "cubicle-element[identifier='#{signed_stream_identifier(resource.to_global_id.to_s)}']",
10
- detail: {
11
- users: Cubism.user_class.find(resource.present_users.members).map { |user| user.slice(user.cubicle_attributes) }.as_json
12
- }
13
- ).broadcast_to(resource)
14
- end
15
- end
@@ -1,11 +0,0 @@
1
- class Cubism::StreamPresenceJob < ApplicationJob
2
- include CableReady::Broadcaster
3
- queue_as :default
4
-
5
- def perform(resource:)
6
- cable_ready[Cubism::PresenceChannel].outer_html(
7
- selector: dom_id(resource, "cubicle").to_s,
8
- html: ApplicationController.render(partial: "shared/presence_indicator", locals: {users: User.where(id: resource.present_users.members)})
9
- ).broadcast
10
- end
11
- end