message_train 0.7.5 → 1.0.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.
Files changed (171) hide show
  1. checksums.yaml +5 -5
  2. data/.gitattributes +1 -0
  3. data/.github/workflows/ci.yml +86 -0
  4. data/.gitignore +64 -0
  5. data/.rubocop.yml +9 -0
  6. data/.rubocop_todo.yml +854 -2
  7. data/.ruby-version +1 -1
  8. data/.simplecov +2 -4
  9. data/CHANGELOG.md +128 -0
  10. data/CLAUDE.md +5 -0
  11. data/Gemfile +30 -32
  12. data/LICENSE.txt +1 -1
  13. data/README.md +106 -35
  14. data/Rakefile +4 -18
  15. data/app/assets/stylesheets/message_train.scss +405 -67
  16. data/app/controllers/concerns/message_train_support.rb +2 -2
  17. data/app/controllers/message_train/boxes_controller.rb +4 -2
  18. data/app/controllers/message_train/conversations_controller.rb +1 -0
  19. data/app/helpers/message_train/application_helper.rb +68 -0
  20. data/app/helpers/message_train/attachments_helper.rb +7 -4
  21. data/app/helpers/message_train/conversations_helper.rb +16 -16
  22. data/app/helpers/message_train/messages_helper.rb +23 -10
  23. data/app/javascript/controllers/message_train/bulk_actions_controller.js +36 -0
  24. data/app/javascript/controllers/message_train/lightbox_controller.js +25 -0
  25. data/app/javascript/controllers/message_train/nested_form_controller.js +36 -0
  26. data/app/javascript/controllers/message_train/recipient_autocomplete_controller.js +31 -0
  27. data/app/mailers/message_train/previews/receipt_mailer_preview.rb +7 -5
  28. data/app/models/message_train/attachment.rb +54 -42
  29. data/app/models/message_train/box.rb +2 -2
  30. data/app/models/message_train/conversation.rb +28 -2
  31. data/app/models/message_train/ignore.rb +2 -2
  32. data/app/models/message_train/message.rb +10 -4
  33. data/app/models/message_train/receipt.rb +27 -4
  34. data/app/models/message_train/unsubscribe.rb +1 -1
  35. data/app/views/application/404.html.haml +1 -3
  36. data/app/views/kaminari/_first_page.html.haml +1 -1
  37. data/app/views/kaminari/_last_page.html.haml +1 -1
  38. data/app/views/kaminari/_next_page.html.haml +1 -1
  39. data/app/views/kaminari/_page.html.haml +3 -3
  40. data/app/views/kaminari/_paginator.html.haml +12 -11
  41. data/app/views/kaminari/_prev_page.html.haml +1 -1
  42. data/app/views/message_train/application/_attachment_fields.html.haml +10 -7
  43. data/app/views/message_train/application/_attachment_link.html.haml +2 -2
  44. data/app/views/message_train/application/_widget.html.haml +2 -4
  45. data/app/views/message_train/boxes/_widget.html.haml +5 -5
  46. data/app/views/message_train/boxes/show.html.haml +28 -45
  47. data/app/views/message_train/collectives/_widget.html.haml +4 -4
  48. data/app/views/message_train/conversations/_conversation.html.haml +11 -11
  49. data/app/views/message_train/conversations/_deleted_toggle.html.haml +1 -1
  50. data/app/views/message_train/conversations/_ignored_toggle.html.haml +2 -2
  51. data/app/views/message_train/conversations/_read_toggle.html.haml +2 -2
  52. data/app/views/message_train/conversations/_toggle.html.haml +6 -4
  53. data/app/views/message_train/conversations/_trashed_toggle.html.haml +2 -2
  54. data/app/views/message_train/conversations/show.html.haml +8 -6
  55. data/app/views/message_train/messages/_deleted_toggle.html.haml +1 -1
  56. data/app/views/message_train/messages/_form.html.haml +22 -30
  57. data/app/views/message_train/messages/_message.html.haml +27 -45
  58. data/app/views/message_train/messages/_read_toggle.html.haml +2 -2
  59. data/app/views/message_train/messages/_toggle.html.haml +5 -1
  60. data/app/views/message_train/messages/_trashed_toggle.html.haml +2 -2
  61. data/app/views/message_train/unsubscribes/index.html.haml +12 -15
  62. data/badges/coverage.json +1 -0
  63. data/config/importmap.rb +2 -0
  64. data/config/locales/en.yml +6 -3
  65. data/db/migrate/20150721145319_create_message_train_conversations.rb +1 -1
  66. data/db/migrate/20150721160322_create_message_train_messages.rb +1 -1
  67. data/db/migrate/20150721161144_create_message_train_attachments.rb +5 -2
  68. data/db/migrate/20150721161940_create_message_train_receipts.rb +1 -1
  69. data/db/migrate/20150721163838_create_message_train_ignores.rb +1 -1
  70. data/db/migrate/20150901183458_add_received_through_to_message_train_receipts.rb +1 -1
  71. data/db/migrate/20151004184347_add_unique_index_to_receipts.rb +1 -1
  72. data/db/migrate/20151124000820_create_message_train_unsubscribes.rb +1 -1
  73. data/db/migrate/20260827095938_remove_paperclip_columns_from_message_train_attachments.rb +8 -0
  74. data/db/migrate/20260827171549_remove_body_from_message_train_messages.rb +5 -0
  75. data/db/migrate/20260827180000_add_marking_indexes.rb +9 -0
  76. data/db/migrate/20260827200000_add_body_back_to_message_train_messages.rb +5 -0
  77. data/docs/screenshots/inbox-desktop.png +0 -0
  78. data/docs/screenshots/inbox-mobile.png +0 -0
  79. data/lib/generators/message_train/install/install_generator.rb +1 -1
  80. data/lib/message_train/engine.rb +11 -0
  81. data/lib/message_train/instance_methods.rb +13 -7
  82. data/lib/message_train/localization.rb +4 -3
  83. data/lib/message_train/version.rb +1 -1
  84. data/lib/message_train.rb +0 -2
  85. data/message_train.gemspec +57 -389
  86. data/spec/controllers/message_train/boxes_controller_spec.rb +13 -8
  87. data/spec/controllers/message_train/concerns_spec.rb +2 -2
  88. data/spec/controllers/message_train/conversations_controller_spec.rb +36 -7
  89. data/spec/controllers/message_train/messages_controller_spec.rb +4 -4
  90. data/spec/dummy/app/assets/stylesheets/application.scss +141 -12
  91. data/spec/dummy/app/controllers/application_controller.rb +1 -1
  92. data/spec/dummy/app/javascript/application.js +4 -0
  93. data/spec/dummy/app/javascript/controllers/application.js +9 -0
  94. data/spec/dummy/app/javascript/controllers/index.js +4 -0
  95. data/spec/dummy/app/models/role.rb +1 -1
  96. data/spec/dummy/app/views/active_storage/blobs/_blob.html.erb +14 -0
  97. data/spec/dummy/app/views/layouts/_top_navigation.html.haml +18 -7
  98. data/spec/dummy/app/views/layouts/application.html.haml +22 -26
  99. data/spec/dummy/bin/importmap +4 -0
  100. data/spec/dummy/config/application.rb +7 -20
  101. data/spec/dummy/config/environments/development.rb +17 -21
  102. data/spec/dummy/config/environments/production.rb +11 -63
  103. data/spec/dummy/config/environments/test.rb +21 -16
  104. data/spec/dummy/config/importmap.rb +7 -0
  105. data/spec/dummy/config/initializers/assets.rb +1 -6
  106. data/spec/dummy/config/routes.rb +1 -1
  107. data/spec/dummy/config/storage.yml +7 -0
  108. data/spec/dummy/db/migrate/20150721140013_devise_create_users.rb +1 -1
  109. data/spec/dummy/db/migrate/20150721141009_rolify_create_roles.rb +1 -1
  110. data/spec/dummy/db/migrate/20150721141128_create_groups.rb +1 -1
  111. data/spec/dummy/db/migrate/20150721150307_add_display_name_column_to_users.rb +1 -1
  112. data/spec/dummy/db/migrate/20160207190402_create_message_train_conversations.message_train.rb +1 -1
  113. data/spec/dummy/db/migrate/20160207190403_create_message_train_messages.message_train.rb +1 -1
  114. data/spec/dummy/db/migrate/20160207190404_create_message_train_attachments.message_train.rb +5 -2
  115. data/spec/dummy/db/migrate/20160207190405_create_message_train_receipts.message_train.rb +1 -1
  116. data/spec/dummy/db/migrate/20160207190406_create_message_train_ignores.message_train.rb +1 -1
  117. data/spec/dummy/db/migrate/20160207190407_add_received_through_to_message_train_receipts.message_train.rb +1 -1
  118. data/spec/dummy/db/migrate/20160207190408_add_unique_index_to_receipts.message_train.rb +1 -1
  119. data/spec/dummy/db/migrate/20160207190409_create_message_train_unsubscribes.message_train.rb +1 -1
  120. data/spec/dummy/db/migrate/20260827095939_remove_paperclip_columns_from_message_train_attachments.message_train.rb +9 -0
  121. data/spec/dummy/db/migrate/20260827161141_create_active_storage_tables.active_storage.rb +57 -0
  122. data/spec/dummy/db/migrate/20260827171550_remove_body_from_message_train_messages.message_train.rb +6 -0
  123. data/spec/dummy/db/migrate/20260827225116_add_marking_indexes.message_train.rb +10 -0
  124. data/spec/dummy/db/migrate/20260828034830_add_body_back_to_message_train_messages.message_train.rb +6 -0
  125. data/spec/dummy/db/migrate/20260828040000_drop_action_text_tables.rb +5 -0
  126. data/spec/dummy/db/schema.rb +106 -72
  127. data/spec/dummy/db/seeds/conversations.seeds.rb +22 -22
  128. data/spec/dummy/db/seeds/development/conversations.seeds.rb +2 -2
  129. data/spec/dummy/db/seeds/groups.seeds.rb +4 -4
  130. data/spec/dummy/db/seeds/test/attachments.seeds.rb +4 -4
  131. data/spec/dummy/db/seeds/test/conversations.seeds.rb +2 -2
  132. data/spec/dummy/db/seeds/users.seeds.rb +8 -8
  133. data/spec/dummy/vendor/javascript/.keep +0 -0
  134. data/spec/factories/attachment.rb +5 -6
  135. data/spec/factories/group.rb +2 -2
  136. data/spec/factories/message.rb +7 -7
  137. data/spec/factories/user.rb +3 -3
  138. data/spec/features/boxes_spec.rb +38 -34
  139. data/spec/features/conversations_spec.rb +13 -6
  140. data/spec/features/messages_spec.rb +20 -20
  141. data/spec/features/unsubscribes_spec.rb +9 -9
  142. data/spec/helpers/message_train/application_helper_spec.rb +16 -18
  143. data/spec/helpers/message_train/attachment_helper_spec.rb +8 -18
  144. data/spec/helpers/message_train/boxes_helper_spec.rb +6 -6
  145. data/spec/helpers/message_train/collectives_helper_spec.rb +10 -10
  146. data/spec/helpers/message_train/conversations_helper_spec.rb +43 -85
  147. data/spec/helpers/message_train/messages_helper_spec.rb +41 -61
  148. data/spec/models/message_train/attachment_spec.rb +26 -36
  149. data/spec/models/message_train/box_spec.rb +2 -2
  150. data/spec/models/message_train/conversation_spec.rb +15 -0
  151. data/spec/models/message_train/message_spec.rb +5 -1
  152. data/spec/models/message_train/receipt_spec.rb +1 -1
  153. data/spec/models/message_train/unsubscribe_spec.rb +1 -1
  154. data/spec/rails_helper.rb +14 -13
  155. data/spec/support/controller_behaviors.rb +13 -13
  156. data/spec/support/factory_bot.rb +3 -0
  157. data/spec/support/feature_behaviors.rb +14 -14
  158. data/spec/support/loaded_site/attachments.rb +6 -2
  159. data/spec/support/utilities.rb +49 -20
  160. metadata +260 -189
  161. data/.travis.yml +0 -12
  162. data/VERSION +0 -1
  163. data/app/assets/javascripts/ckeditor/config.js +0 -49
  164. data/app/assets/javascripts/message_train.js +0 -149
  165. data/spec/dummy/app/assets/javascripts/application.js +0 -17
  166. data/spec/dummy/app/assets/stylesheets/bootstrap-everything.scss +0 -54
  167. data/spec/dummy/config/initializers/bootstrap_leather.rb +0 -7
  168. data/spec/dummy/config/initializers/paperclip.rb +0 -5
  169. data/spec/dummy/config/secrets.yml +0 -22
  170. data/spec/dummy/db/test.sqlite3 +0 -0
  171. data/spec/support/factory_girl.rb +0 -3
@@ -1,11 +1,66 @@
1
- @import 'tokenfield-typeahead';
2
- @import 'bootstrap-tokenfield';
1
+ // Design tokens. Custom properties are global by nature (unlike a scoped
2
+ // class), so names are prefixed the same way message-train-* classes are -
3
+ // a host app's own --space-sm shouldn't silently collide with ours.
4
+ :root {
5
+ // Type scale (TYPOGRAPHY.md): base 1rem, 1.25 ratio, one deliberate
6
+ // exception below base (0.875rem lands on a clean 14px; strict ratio
7
+ // math would give 0.8rem).
8
+ --message-train-fs-sm: 0.875rem;
9
+ --message-train-fs-base: 1rem;
10
+ --message-train-fs-lg: 1.25rem;
11
+ --message-train-fs-xl: 1.563rem;
12
+ --message-train-fs-2xl: 1.953rem;
3
13
 
4
- img {
14
+ // Spacing scale (CSS.md): clean multiples/fractions of the type scale's
15
+ // own base unit, so spacing and type can never drift apart.
16
+ --message-train-space-3xs: 0.125rem;
17
+ --message-train-space-xs: 0.25rem;
18
+ --message-train-space-sm: 0.5rem;
19
+ --message-train-space-md: 1rem;
20
+ --message-train-space-lg: 1.5rem;
21
+ --message-train-space-xl: 2rem;
22
+ --message-train-space-2xl: 3rem;
23
+
24
+ // Color palette
25
+ --message-train-color-accent: #337ab7;
26
+ --message-train-color-accent-contrast: #fff;
27
+ --message-train-color-border: #ccc;
28
+ --message-train-color-border-subtle: #eee;
29
+ --message-train-color-surface: #f7f7f7;
30
+ --message-train-color-surface-hover: #eee;
31
+ --message-train-color-surface-elevated: #fff;
32
+ --message-train-color-row-read: #f2f2f2;
33
+ --message-train-color-row-selected: #fcf8e3;
34
+ --message-train-color-danger: #d9534f;
35
+ }
36
+
37
+ .message-train-lightbox-image,
38
+ .message-train-thumbnail {
5
39
  max-width: 100%;
6
40
  }
7
41
 
8
- .attachment-thumbnails {
42
+ .message-train-icon {
43
+ display: inline-block;
44
+ // Icon glyphs are emoji, and different emoji can carry different
45
+ // built-in line-height metrics depending on the browser/OS font
46
+ // fallback - left on "normal", that can make two buttons with
47
+ // otherwise-identical padding/font-size render at different heights
48
+ // depending purely on which emoji happens to be inside. Pinning it
49
+ // makes the icon's contribution to line height deterministic.
50
+ line-height: 1;
51
+ margin-right: var(--message-train-space-xs);
52
+ }
53
+
54
+ .message-train-badge {
55
+ display: inline-block;
56
+ padding: var(--message-train-space-3xs) var(--message-train-space-sm);
57
+ border-radius: 1em;
58
+ background-color: var(--message-train-color-accent);
59
+ color: var(--message-train-color-accent-contrast);
60
+ font-size: var(--message-train-fs-sm);
61
+ }
62
+
63
+ .message-train-attachment-thumbnails {
9
64
  text-align: center;
10
65
 
11
66
  .thumbnail {
@@ -13,11 +68,90 @@ img {
13
68
  }
14
69
  }
15
70
 
71
+ // The thread page's list of individual messages - laid out as a grid so
72
+ // each message reads as its own separated card, with the grid gap itself
73
+ // providing the separation instead of per-card margins.
74
+ .message-train-messages {
75
+ display: grid;
76
+ gap: var(--message-train-space-md);
77
+ }
78
+
79
+ .message-train-message {
80
+ padding: var(--message-train-space-md);
81
+ border: 1px solid var(--message-train-color-border);
82
+ border-radius: var(--message-train-space-xs);
83
+ background: var(--message-train-color-surface-elevated);
84
+ }
85
+
16
86
  .message-train-conversations {
87
+ width: 100%;
88
+ border-collapse: collapse;
89
+
90
+ // Mobile-first (RESPONSIVE.md): a real table row is too wide to fit a
91
+ // narrow screen without horizontal scrolling, so below the breakpoint
92
+ // each row becomes a small CSS grid "card" instead - sender/subject/
93
+ // date/etc. wrap onto their own lines within the row's own width
94
+ // rather than the row extending past the viewport. Real table layout
95
+ // returns once there's room, at the same breakpoint used elsewhere.
96
+ tr.message_train_conversation {
97
+ display: grid;
98
+ grid-template-columns: auto 1fr auto;
99
+ grid-template-areas:
100
+ 'checkbox sender attachment'
101
+ 'checkbox subject subject'
102
+ 'checkbox date actions';
103
+ align-items: center;
104
+ column-gap: var(--message-train-space-sm);
105
+ row-gap: var(--message-train-space-3xs);
106
+ padding: var(--message-train-space-sm) var(--message-train-space-md);
107
+ border-bottom: 1px solid var(--message-train-color-border-subtle);
108
+ }
109
+
110
+ td {
111
+ padding: 0;
112
+ vertical-align: middle;
113
+ border-bottom: none;
114
+ }
115
+
116
+ .message-train-conversation-checkbox {
117
+ grid-area: checkbox;
118
+ }
119
+
120
+ .message-train-conversation-sender {
121
+ grid-area: sender;
122
+ }
123
+
124
+ .message-train-conversation-subject {
125
+ grid-area: subject;
126
+ }
127
+
128
+ .message-train-conversation-attachment {
129
+ grid-area: attachment;
130
+ }
131
+
132
+ .message-train-date-column {
133
+ grid-area: date;
134
+ }
135
+
136
+ .message-train-conversation-actions {
137
+ grid-area: actions;
138
+ }
139
+
140
+ @media (min-width: 48rem) {
141
+ tr.message_train_conversation {
142
+ display: table-row;
143
+ }
144
+
145
+ td {
146
+ padding: var(--message-train-space-sm) var(--message-train-space-md);
147
+ border-bottom: 1px solid var(--message-train-color-border-subtle);
148
+ }
149
+ }
150
+
17
151
  a,
18
152
  a:link,
19
153
  a:visited {
20
- color: $text-color;
154
+ color: inherit;
21
155
  }
22
156
 
23
157
  a:hover {
@@ -25,112 +159,316 @@ img {
25
159
  }
26
160
 
27
161
  input[type='checkbox'] {
28
- margin-right: 1em;
162
+ margin-right: var(--message-train-space-md);
163
+ }
164
+
165
+ // Zebra/tiger striping so adjacent rows stay visually separated even
166
+ // when they share the same read state (several unread rows in a row
167
+ // otherwise all render on the same plain background and blend
168
+ // together). Read/selected below override this per-row regardless of
169
+ // position - matched specificity (tr.message_train_conversation.___)
170
+ // plus coming later in the source is what makes that override work.
171
+ tr.message_train_conversation:nth-child(even) {
172
+ background-color: var(--message-train-color-surface);
29
173
  }
30
174
 
31
175
  .unread {
32
176
  font-weight: bold;
33
177
  }
34
178
 
35
- .read {
36
- background-color: rgba($list-group-disabled-bg, .75);
37
-
38
- .flicker.flicker-out {
39
- background-color: $list-group-disabled-bg;
40
- transition: background-color 1s ease-out;
41
- }
179
+ tr.message_train_conversation.read {
180
+ background-color: var(--message-train-color-row-read);
42
181
  }
43
182
 
44
183
  .draft {
45
184
  font-style: italic;
46
185
  }
47
186
 
48
- .selected,
49
- .read.selected {
50
- background-color: $state-warning-bg;
51
-
52
- a {
53
- color: $state-warning-text;
54
- }
187
+ tr.message_train_conversation.selected,
188
+ tr.message_train_conversation.read.selected {
189
+ background-color: var(--message-train-color-row-selected);
55
190
  }
191
+ }
56
192
 
57
- .flicker,
58
- .read.flicker,
59
- .selected.flicker {
60
- background-color: $state-info-bg;
61
- transition: background-color .1s ease-in;
62
- }
193
+ .message-train-box-actions {
194
+ display: flex;
195
+ flex-direction: column;
196
+ gap: var(--message-train-space-sm);
197
+ margin: var(--message-train-space-xl) 0 var(--message-train-space-md);
198
+ }
63
199
 
64
- .flicker.flicker-out,
65
- .unread.flicker.flicker-out {
66
- background-color: $list-group-bg;
67
- transition: background-color 1s ease-out;
68
- }
200
+ .message-train-box-actions-row {
201
+ display: flex;
202
+ align-items: center;
203
+ flex-wrap: wrap;
204
+ gap: var(--message-train-space-md);
69
205
  }
70
206
 
71
- .box-actions {
72
- margin: 2em 0 1em;
207
+ // Doubled class rather than plain .message-train-button: these render as
208
+ // <a>, <button>, and <input type="submit"> across different call sites,
209
+ // and a host app commonly has its own generic reset for at least one of
210
+ // those tags (e.g. button[type="submit"]) - which sits at the same or a
211
+ // higher specificity tier than a single class, and (since a host app's
212
+ // own stylesheet almost always loads after this one) would silently win
213
+ // that tie. Repeating the class costs nothing and reliably beats a
214
+ // single class-or-attribute-qualified host rule regardless of load
215
+ // order, with no !important needed.
216
+ .message-train-button.message-train-button {
217
+ // Without this, .message-train-btn-compose's `width: 100%` sizes only
218
+ // the content box, then adds padding/border on top - overflowing its
219
+ // container by the padding amount (found while sizing the sidebar
220
+ // compose button: it rendered ~66px wider than the column it was in).
221
+ box-sizing: border-box;
222
+ display: inline-flex;
223
+ align-items: center;
224
+ // Pinned rather than "normal" for the same reason as .message-train-icon
225
+ // below - a button whose only difference from another is which emoji
226
+ // icon it contains shouldn't end up a different height because that
227
+ // emoji happens to carry different line-height metrics in some
228
+ // browser/OS font fallback.
229
+ line-height: 1.2;
230
+ padding: var(--message-train-space-sm) var(--message-train-space-md);
231
+ border: 1px solid var(--message-train-color-border);
232
+ border-radius: var(--message-train-space-xs);
233
+ background: var(--message-train-color-surface);
234
+ color: inherit;
235
+ text-decoration: none;
236
+ cursor: pointer;
237
+ font-size: var(--message-train-fs-base);
238
+ }
73
239
 
74
- .check-all button {
75
- min-height: 2.5em;
76
- }
240
+ .message-train-button.message-train-button:hover {
241
+ background: var(--message-train-color-surface-hover);
77
242
  }
78
243
 
79
- div.conversation-actions {
80
- margin: 1em 0;
244
+ .message-train-button--small {
245
+ padding: var(--message-train-space-xs) var(--message-train-space-sm);
246
+ font-size: var(--message-train-fs-sm);
81
247
  }
82
248
 
83
- td.conversation-actions {
84
- white-space: nowrap;
249
+ .message-train-button--large {
250
+ padding: var(--message-train-space-md) var(--message-train-space-xl);
251
+ font-size: var(--message-train-fs-lg);
85
252
  }
86
253
 
87
- .date-column {
88
- white-space: nowrap;
254
+ // Emphasize the primary action in a pair of same-size, same-shape
255
+ // buttons (e.g. Send vs. Save As Draft) with color rather than size, so
256
+ // they read as equally substantial actions and only differ in which one
257
+ // is the recommended path forward.
258
+ .message-train-button--primary {
259
+ border-color: var(--message-train-color-accent);
260
+ background: var(--message-train-color-accent);
261
+ color: var(--message-train-color-accent-contrast);
262
+ }
263
+
264
+ .message-train-button--primary:hover {
265
+ background: var(--message-train-color-accent);
266
+ opacity: 0.9;
267
+ }
268
+
269
+ // Add/remove attachment - pill-shaped, outline-only, colored by intent
270
+ // (accent for adding, danger for removing) rather than filled, so they
271
+ // read as lightweight row-level actions instead of primary form actions.
272
+ .message-train-button--add-attachment,
273
+ .message-train-button--remove-attachment {
274
+ border-radius: 999px;
275
+ background: none;
89
276
  }
90
277
 
91
- .glyphicon-thumbnail {
92
- font-size: 170px;
278
+ .message-train-button--add-attachment {
279
+ border-color: var(--message-train-color-accent);
280
+ color: var(--message-train-color-accent);
93
281
  }
94
282
 
95
- .glyphicon.spinning {
96
- -webkit-animation: spin2 1s infinite linear;
97
- animation: spin 1s infinite linear;
283
+ .message-train-button--add-attachment:hover {
284
+ background: var(--message-train-color-accent);
285
+ color: var(--message-train-color-accent-contrast);
98
286
  }
99
287
 
100
- @keyframes spin {
101
- from { transform: scale(1) rotate(0deg);}
102
- to { transform: scale(1) rotate(360deg);}
288
+ .message-train-button--remove-attachment {
289
+ border-color: var(--message-train-color-danger);
290
+ color: var(--message-train-color-danger);
103
291
  }
104
292
 
105
- @-webkit-keyframes spin2 {
106
- from { -webkit-transform: rotate(0deg);}
107
- to { -webkit-transform: rotate(360deg);}
293
+ .message-train-button--remove-attachment:hover {
294
+ background: var(--message-train-color-danger);
295
+ color: var(--message-train-color-accent-contrast);
108
296
  }
109
297
 
110
- .clear-left {
111
- clear: left;
298
+ .message-train-select-actions {
299
+ display: flex;
300
+ flex-wrap: wrap;
301
+ gap: var(--message-train-space-sm);
112
302
  }
113
303
 
114
- .clear-right {
115
- clear: right;
304
+ .message-train-message-actions {
305
+ float: right;
306
+ display: flex;
307
+ gap: var(--message-train-space-sm);
116
308
  }
117
309
 
118
- .clear-both {
119
- clear: both;
310
+ .message-train-mark-actions {
311
+ display: flex;
312
+ flex-wrap: wrap;
313
+ gap: var(--message-train-space-sm);
120
314
  }
121
315
 
122
- .btn-compose {
123
- margin: 0 auto 10px;
316
+ div.message-train-conversation-actions {
317
+ margin: var(--message-train-space-md) 0;
318
+ }
319
+
320
+ td.message-train-conversation-actions {
321
+ white-space: nowrap;
322
+ }
323
+
324
+ .message-train-date-column {
325
+ white-space: nowrap;
326
+ }
327
+
328
+ .message-train-btn-compose {
329
+ margin: 0 auto var(--message-train-space-sm);
124
330
  white-space: inherit;
125
331
  width: 100%;
126
332
  }
127
333
 
128
- .tt-menu {
129
- @extend .tt-dropdown-menu;
130
- border-top-left-radius: 0;
131
- border-top-right-radius: 0;
334
+ // The boxes/collectives sidebar widget (compose button + box list) -
335
+ // meant to render alongside a page's main content, not above/below it;
336
+ // see the dummy app's own application.scss for the page-level grid that
337
+ // actually places it as a sidebar. Not meaningful on a printed page.
338
+ .message-train-widget {
339
+ display: flex;
340
+ flex-direction: column;
341
+ gap: var(--message-train-space-md);
342
+
343
+ h3 {
344
+ margin: 0;
345
+ font-size: var(--message-train-fs-lg);
346
+ font-weight: 600;
347
+ }
348
+
349
+ @media print {
350
+ display: none;
351
+ }
352
+ }
353
+
354
+ .message-train-list-group {
355
+ list-style: none;
356
+ margin: 0;
357
+ padding: 0;
358
+ border: 1px solid var(--message-train-color-border);
359
+ border-radius: var(--message-train-space-xs);
360
+ overflow: hidden;
361
+ }
362
+
363
+ .message-train-list-group-item {
364
+ display: flex;
365
+ align-items: center;
366
+ justify-content: space-between;
367
+ gap: var(--message-train-space-sm);
368
+ padding: var(--message-train-space-sm) var(--message-train-space-md);
369
+ border-bottom: 1px solid var(--message-train-color-border-subtle);
370
+
371
+ &:last-child {
372
+ border-bottom: none;
373
+ }
374
+
375
+ a {
376
+ color: inherit;
377
+ text-decoration: none;
378
+ }
379
+ }
380
+
381
+ // Plain <details>/<summary> - click/tap-to-toggle and keyboard support
382
+ // come for free, with no JS required. The popover (ul) is explicitly
383
+ // hidden by default rather than relying on the browser's own
384
+ // closed-<details> hiding: giving it `position: absolute` (so it floats
385
+ // over page content without affecting this element's own box) means
386
+ // it's no longer an in-flow child, and at least one tested engine
387
+ // renders it unconditionally in that case instead of respecting the
388
+ // native closed state.
389
+ .message-train-dropdown-nav-item {
390
+ position: relative;
391
+
392
+ ul {
393
+ display: none;
394
+ position: absolute;
395
+ z-index: 10;
396
+ min-width: 15rem;
397
+ list-style: none;
398
+ margin: 0;
399
+ padding: var(--message-train-space-sm);
400
+ background: var(--message-train-color-surface-elevated);
401
+ border: 1px solid var(--message-train-color-border);
402
+ }
403
+
404
+ &[open] ul {
405
+ display: block;
406
+ }
132
407
  }
133
408
 
134
- .dropdown-menu {
135
- width: 240px;
409
+ // The message compose/reply form (recipient, subject, body, attachments).
410
+ // Scoped under .message-train-form rather than bare input/textarea/label
411
+ // selectors, so these rules can't leak onto unrelated forms elsewhere on
412
+ // a host app's page - the same collision-avoidance reasoning as the
413
+ // message-train- class prefix itself.
414
+ .message-train-form {
415
+ label {
416
+ display: block;
417
+ margin-bottom: var(--message-train-space-xs);
418
+ font-weight: 600;
419
+ }
420
+
421
+ input[type='text'],
422
+ input[type='email'],
423
+ textarea {
424
+ box-sizing: border-box;
425
+ width: 100%;
426
+ padding: var(--message-train-space-sm) var(--message-train-space-md);
427
+ border: 1px solid var(--message-train-color-border);
428
+ border-radius: var(--message-train-space-xs);
429
+ background: var(--message-train-color-surface-elevated);
430
+ font-size: var(--message-train-fs-base);
431
+ font-family: inherit;
432
+ color: inherit;
433
+ outline: none;
434
+ transition: border-color 0.15s;
435
+ }
436
+
437
+ input[type='text']:focus,
438
+ input[type='email']:focus,
439
+ textarea:focus {
440
+ border-color: var(--message-train-color-accent);
441
+ }
442
+
443
+ textarea {
444
+ resize: vertical;
445
+ min-height: 5rem;
446
+ line-height: 1.6;
447
+ }
448
+
449
+ input[type='file'] {
450
+ font-size: var(--message-train-fs-base);
451
+ }
452
+
453
+ // The file input's own picker button - styled to match
454
+ // .message-train-button rather than the browser's default appearance,
455
+ // since ::file-selector-button can't take a class of its own.
456
+ input[type='file']::file-selector-button {
457
+ margin-right: var(--message-train-space-sm);
458
+ padding: var(--message-train-space-xs) var(--message-train-space-md);
459
+ border: 1px solid var(--message-train-color-border);
460
+ border-radius: var(--message-train-space-xs);
461
+ background: var(--message-train-color-surface);
462
+ font-size: var(--message-train-fs-base);
463
+ font-family: inherit;
464
+ color: inherit;
465
+ cursor: pointer;
466
+ }
467
+
468
+ .message-train-hint {
469
+ display: block;
470
+ margin-bottom: var(--message-train-space-xs);
471
+ font-size: var(--message-train-fs-sm);
472
+ opacity: 0.75;
473
+ }
136
474
  }
@@ -91,7 +91,7 @@ module MessageTrainSupport
91
91
  respond_to do |format|
92
92
  format.html do
93
93
  flash[:error] = @box.message
94
- show
94
+ redirect_to request.path
95
95
  end
96
96
  format.json { render :results, status: :unprocessable_entity }
97
97
  end
@@ -102,7 +102,7 @@ module MessageTrainSupport
102
102
  format.html do
103
103
  flash_type = @box.results.any? ? :notice : :alert
104
104
  flash[flash_type] = @box.message
105
- show
105
+ redirect_to request.path
106
106
  end
107
107
  format.json { render :results, status: :accepted }
108
108
  end
@@ -5,8 +5,10 @@ module MessageTrain
5
5
 
6
6
  # GET /box/:division
7
7
  def show
8
- @conversations = @conversations.order(updated_at: :desc)
9
- .page(params[:page])
8
+ @conversations = @conversations
9
+ .includes(:ignores, :attachments, messages: :sender)
10
+ .order(updated_at: :desc)
11
+ .page(params[:page])
10
12
  render :show
11
13
  end
12
14
 
@@ -7,6 +7,7 @@ module MessageTrain
7
7
  # GET /box/:division/conversations/:id
8
8
  def show
9
9
  @messages = @conversation.messages
10
+ .includes(:sender, :attachments)
10
11
  .order(updated_at: :desc)
11
12
  .page(params[:page])
12
13
  render :show
@@ -1,10 +1,30 @@
1
1
  module MessageTrain
2
2
  # Application helper
3
3
  module ApplicationHelper
4
+ ICON_LABELS = {
5
+ 'plus' => '+',
6
+ 'trash' => '🗑',
7
+ 'inbox' => '📥',
8
+ 'eye-open' => '👁',
9
+ 'eye-close' => '🚫',
10
+ 'remove' => '✕',
11
+ 'volume-up' => '🔔',
12
+ 'volume-off' => '🔕',
13
+ 'paperclip' => '📎',
14
+ 'refresh spinning' => '⟳',
15
+ 'check' => '✓'
16
+ }.freeze
17
+
4
18
  def message_train_widget
5
19
  render partial: 'message_train/application/widget'
6
20
  end
7
21
 
22
+ FLASH_ALERT_CLASSES = { notice: 'info', alert: 'warning', error: 'danger' }.freeze
23
+
24
+ def flash_alert_class(flash_type)
25
+ FLASH_ALERT_CLASSES.fetch(flash_type.to_sym, flash_type.to_s)
26
+ end
27
+
8
28
  def fuzzy_date(date)
9
29
  time = Time.parse(date.strftime('%F %T'))
10
30
  change_in_time = Time.now - time
@@ -12,6 +32,54 @@ module MessageTrain
12
32
  l(time, format: fuzzy_date_format(change_in_time))
13
33
  end
14
34
 
35
+ # Replacements for the bootstrap_leather helpers the views used to
36
+ # depend on, backed by plain semantic HTML instead of Bootstrap markup.
37
+
38
+ def add_title(text)
39
+ content_for(:title) { text }
40
+ end
41
+
42
+ def add_subtitle(text)
43
+ content_for(:subtitle) { text }
44
+ end
45
+
46
+ def icon(name, extra_class: nil)
47
+ content_tag(
48
+ :span, ICON_LABELS.fetch(name, ''),
49
+ class: ['message-train-icon', extra_class].compact.join(' '),
50
+ aria: { hidden: 'true' }
51
+ )
52
+ end
53
+
54
+ def badge(text, extra_class = nil)
55
+ content_tag(
56
+ :span, text, class: ['message-train-badge', extra_class].compact.join(' ')
57
+ )
58
+ end
59
+
60
+ def nav_item(text, link, options = {})
61
+ content_tag(:li, link_to(text, link, options))
62
+ end
63
+
64
+ def dropdown_nav_item(text, _url, &block)
65
+ content_tag(:details, class: 'message-train-dropdown-nav-item') do
66
+ content_tag(:summary, text) + content_tag(:ul, capture(&block), class: 'message-train-dropdown-menu')
67
+ end
68
+ end
69
+
70
+ def icon_button_to(extra_class, icon_name, text, url, options = {})
71
+ button_to(
72
+ icon(icon_name) + text, url,
73
+ options.merge(class: [options[:class], extra_class].compact.join(' '))
74
+ )
75
+ end
76
+
77
+ def modal(id, title, &block)
78
+ content_tag(:dialog, id: id) do
79
+ content_tag(:h2, title) + capture(&block)
80
+ end
81
+ end
82
+
15
83
  private
16
84
 
17
85
  def fuzzy_date_format(change_in_time)
@@ -2,10 +2,13 @@ module MessageTrain
2
2
  # Attachments helper
3
3
  module AttachmentsHelper
4
4
  def attachment_icon(attachment)
5
- return image_tag(attachment.attachment.url(:thumb)) if attachment.image?
6
- icon_class = 'glyphicon glyphicon-save-file glyphicon-thumbnail'
7
- html = content_tag(:span, '', class: icon_class)
8
- html << tag(:br) + attachment.attachment_file_name
5
+ if attachment.image?
6
+ return image_tag(
7
+ main_app.url_for(attachment.thumb), class: 'message-train-thumbnail'
8
+ )
9
+ end
10
+ html = content_tag(:span, '', class: 'message-train-icon icon-file')
11
+ html << tag(:br) + attachment.attachment.filename.to_s
9
12
  html.html_safe
10
13
  end
11
14