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.
- checksums.yaml +5 -5
- data/.gitattributes +1 -0
- data/.github/workflows/ci.yml +86 -0
- data/.gitignore +64 -0
- data/.rubocop.yml +9 -0
- data/.rubocop_todo.yml +854 -2
- data/.ruby-version +1 -1
- data/.simplecov +2 -4
- data/CHANGELOG.md +128 -0
- data/CLAUDE.md +5 -0
- data/Gemfile +30 -32
- data/LICENSE.txt +1 -1
- data/README.md +106 -35
- data/Rakefile +4 -18
- data/app/assets/stylesheets/message_train.scss +405 -67
- data/app/controllers/concerns/message_train_support.rb +2 -2
- data/app/controllers/message_train/boxes_controller.rb +4 -2
- data/app/controllers/message_train/conversations_controller.rb +1 -0
- data/app/helpers/message_train/application_helper.rb +68 -0
- data/app/helpers/message_train/attachments_helper.rb +7 -4
- data/app/helpers/message_train/conversations_helper.rb +16 -16
- data/app/helpers/message_train/messages_helper.rb +23 -10
- data/app/javascript/controllers/message_train/bulk_actions_controller.js +36 -0
- data/app/javascript/controllers/message_train/lightbox_controller.js +25 -0
- data/app/javascript/controllers/message_train/nested_form_controller.js +36 -0
- data/app/javascript/controllers/message_train/recipient_autocomplete_controller.js +31 -0
- data/app/mailers/message_train/previews/receipt_mailer_preview.rb +7 -5
- data/app/models/message_train/attachment.rb +54 -42
- data/app/models/message_train/box.rb +2 -2
- data/app/models/message_train/conversation.rb +28 -2
- data/app/models/message_train/ignore.rb +2 -2
- data/app/models/message_train/message.rb +10 -4
- data/app/models/message_train/receipt.rb +27 -4
- data/app/models/message_train/unsubscribe.rb +1 -1
- data/app/views/application/404.html.haml +1 -3
- data/app/views/kaminari/_first_page.html.haml +1 -1
- data/app/views/kaminari/_last_page.html.haml +1 -1
- data/app/views/kaminari/_next_page.html.haml +1 -1
- data/app/views/kaminari/_page.html.haml +3 -3
- data/app/views/kaminari/_paginator.html.haml +12 -11
- data/app/views/kaminari/_prev_page.html.haml +1 -1
- data/app/views/message_train/application/_attachment_fields.html.haml +10 -7
- data/app/views/message_train/application/_attachment_link.html.haml +2 -2
- data/app/views/message_train/application/_widget.html.haml +2 -4
- data/app/views/message_train/boxes/_widget.html.haml +5 -5
- data/app/views/message_train/boxes/show.html.haml +28 -45
- data/app/views/message_train/collectives/_widget.html.haml +4 -4
- data/app/views/message_train/conversations/_conversation.html.haml +11 -11
- data/app/views/message_train/conversations/_deleted_toggle.html.haml +1 -1
- data/app/views/message_train/conversations/_ignored_toggle.html.haml +2 -2
- data/app/views/message_train/conversations/_read_toggle.html.haml +2 -2
- data/app/views/message_train/conversations/_toggle.html.haml +6 -4
- data/app/views/message_train/conversations/_trashed_toggle.html.haml +2 -2
- data/app/views/message_train/conversations/show.html.haml +8 -6
- data/app/views/message_train/messages/_deleted_toggle.html.haml +1 -1
- data/app/views/message_train/messages/_form.html.haml +22 -30
- data/app/views/message_train/messages/_message.html.haml +27 -45
- data/app/views/message_train/messages/_read_toggle.html.haml +2 -2
- data/app/views/message_train/messages/_toggle.html.haml +5 -1
- data/app/views/message_train/messages/_trashed_toggle.html.haml +2 -2
- data/app/views/message_train/unsubscribes/index.html.haml +12 -15
- data/badges/coverage.json +1 -0
- data/config/importmap.rb +2 -0
- data/config/locales/en.yml +6 -3
- data/db/migrate/20150721145319_create_message_train_conversations.rb +1 -1
- data/db/migrate/20150721160322_create_message_train_messages.rb +1 -1
- data/db/migrate/20150721161144_create_message_train_attachments.rb +5 -2
- data/db/migrate/20150721161940_create_message_train_receipts.rb +1 -1
- data/db/migrate/20150721163838_create_message_train_ignores.rb +1 -1
- data/db/migrate/20150901183458_add_received_through_to_message_train_receipts.rb +1 -1
- data/db/migrate/20151004184347_add_unique_index_to_receipts.rb +1 -1
- data/db/migrate/20151124000820_create_message_train_unsubscribes.rb +1 -1
- data/db/migrate/20260827095938_remove_paperclip_columns_from_message_train_attachments.rb +8 -0
- data/db/migrate/20260827171549_remove_body_from_message_train_messages.rb +5 -0
- data/db/migrate/20260827180000_add_marking_indexes.rb +9 -0
- data/db/migrate/20260827200000_add_body_back_to_message_train_messages.rb +5 -0
- data/docs/screenshots/inbox-desktop.png +0 -0
- data/docs/screenshots/inbox-mobile.png +0 -0
- data/lib/generators/message_train/install/install_generator.rb +1 -1
- data/lib/message_train/engine.rb +11 -0
- data/lib/message_train/instance_methods.rb +13 -7
- data/lib/message_train/localization.rb +4 -3
- data/lib/message_train/version.rb +1 -1
- data/lib/message_train.rb +0 -2
- data/message_train.gemspec +57 -389
- data/spec/controllers/message_train/boxes_controller_spec.rb +13 -8
- data/spec/controllers/message_train/concerns_spec.rb +2 -2
- data/spec/controllers/message_train/conversations_controller_spec.rb +36 -7
- data/spec/controllers/message_train/messages_controller_spec.rb +4 -4
- data/spec/dummy/app/assets/stylesheets/application.scss +141 -12
- data/spec/dummy/app/controllers/application_controller.rb +1 -1
- data/spec/dummy/app/javascript/application.js +4 -0
- data/spec/dummy/app/javascript/controllers/application.js +9 -0
- data/spec/dummy/app/javascript/controllers/index.js +4 -0
- data/spec/dummy/app/models/role.rb +1 -1
- data/spec/dummy/app/views/active_storage/blobs/_blob.html.erb +14 -0
- data/spec/dummy/app/views/layouts/_top_navigation.html.haml +18 -7
- data/spec/dummy/app/views/layouts/application.html.haml +22 -26
- data/spec/dummy/bin/importmap +4 -0
- data/spec/dummy/config/application.rb +7 -20
- data/spec/dummy/config/environments/development.rb +17 -21
- data/spec/dummy/config/environments/production.rb +11 -63
- data/spec/dummy/config/environments/test.rb +21 -16
- data/spec/dummy/config/importmap.rb +7 -0
- data/spec/dummy/config/initializers/assets.rb +1 -6
- data/spec/dummy/config/routes.rb +1 -1
- data/spec/dummy/config/storage.yml +7 -0
- data/spec/dummy/db/migrate/20150721140013_devise_create_users.rb +1 -1
- data/spec/dummy/db/migrate/20150721141009_rolify_create_roles.rb +1 -1
- data/spec/dummy/db/migrate/20150721141128_create_groups.rb +1 -1
- data/spec/dummy/db/migrate/20150721150307_add_display_name_column_to_users.rb +1 -1
- data/spec/dummy/db/migrate/20160207190402_create_message_train_conversations.message_train.rb +1 -1
- data/spec/dummy/db/migrate/20160207190403_create_message_train_messages.message_train.rb +1 -1
- data/spec/dummy/db/migrate/20160207190404_create_message_train_attachments.message_train.rb +5 -2
- data/spec/dummy/db/migrate/20160207190405_create_message_train_receipts.message_train.rb +1 -1
- data/spec/dummy/db/migrate/20160207190406_create_message_train_ignores.message_train.rb +1 -1
- data/spec/dummy/db/migrate/20160207190407_add_received_through_to_message_train_receipts.message_train.rb +1 -1
- data/spec/dummy/db/migrate/20160207190408_add_unique_index_to_receipts.message_train.rb +1 -1
- data/spec/dummy/db/migrate/20160207190409_create_message_train_unsubscribes.message_train.rb +1 -1
- data/spec/dummy/db/migrate/20260827095939_remove_paperclip_columns_from_message_train_attachments.message_train.rb +9 -0
- data/spec/dummy/db/migrate/20260827161141_create_active_storage_tables.active_storage.rb +57 -0
- data/spec/dummy/db/migrate/20260827171550_remove_body_from_message_train_messages.message_train.rb +6 -0
- data/spec/dummy/db/migrate/20260827225116_add_marking_indexes.message_train.rb +10 -0
- data/spec/dummy/db/migrate/20260828034830_add_body_back_to_message_train_messages.message_train.rb +6 -0
- data/spec/dummy/db/migrate/20260828040000_drop_action_text_tables.rb +5 -0
- data/spec/dummy/db/schema.rb +106 -72
- data/spec/dummy/db/seeds/conversations.seeds.rb +22 -22
- data/spec/dummy/db/seeds/development/conversations.seeds.rb +2 -2
- data/spec/dummy/db/seeds/groups.seeds.rb +4 -4
- data/spec/dummy/db/seeds/test/attachments.seeds.rb +4 -4
- data/spec/dummy/db/seeds/test/conversations.seeds.rb +2 -2
- data/spec/dummy/db/seeds/users.seeds.rb +8 -8
- data/spec/dummy/vendor/javascript/.keep +0 -0
- data/spec/factories/attachment.rb +5 -6
- data/spec/factories/group.rb +2 -2
- data/spec/factories/message.rb +7 -7
- data/spec/factories/user.rb +3 -3
- data/spec/features/boxes_spec.rb +38 -34
- data/spec/features/conversations_spec.rb +13 -6
- data/spec/features/messages_spec.rb +20 -20
- data/spec/features/unsubscribes_spec.rb +9 -9
- data/spec/helpers/message_train/application_helper_spec.rb +16 -18
- data/spec/helpers/message_train/attachment_helper_spec.rb +8 -18
- data/spec/helpers/message_train/boxes_helper_spec.rb +6 -6
- data/spec/helpers/message_train/collectives_helper_spec.rb +10 -10
- data/spec/helpers/message_train/conversations_helper_spec.rb +43 -85
- data/spec/helpers/message_train/messages_helper_spec.rb +41 -61
- data/spec/models/message_train/attachment_spec.rb +26 -36
- data/spec/models/message_train/box_spec.rb +2 -2
- data/spec/models/message_train/conversation_spec.rb +15 -0
- data/spec/models/message_train/message_spec.rb +5 -1
- data/spec/models/message_train/receipt_spec.rb +1 -1
- data/spec/models/message_train/unsubscribe_spec.rb +1 -1
- data/spec/rails_helper.rb +14 -13
- data/spec/support/controller_behaviors.rb +13 -13
- data/spec/support/factory_bot.rb +3 -0
- data/spec/support/feature_behaviors.rb +14 -14
- data/spec/support/loaded_site/attachments.rb +6 -2
- data/spec/support/utilities.rb +49 -20
- metadata +260 -189
- data/.travis.yml +0 -12
- data/VERSION +0 -1
- data/app/assets/javascripts/ckeditor/config.js +0 -49
- data/app/assets/javascripts/message_train.js +0 -149
- data/spec/dummy/app/assets/javascripts/application.js +0 -17
- data/spec/dummy/app/assets/stylesheets/bootstrap-everything.scss +0 -54
- data/spec/dummy/config/initializers/bootstrap_leather.rb +0 -7
- data/spec/dummy/config/initializers/paperclip.rb +0 -5
- data/spec/dummy/config/secrets.yml +0 -22
- data/spec/dummy/db/test.sqlite3 +0 -0
- data/spec/support/factory_girl.rb +0 -3
|
@@ -1,11 +1,66 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
.
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
240
|
+
.message-train-button.message-train-button:hover {
|
|
241
|
+
background: var(--message-train-color-surface-hover);
|
|
77
242
|
}
|
|
78
243
|
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
.
|
|
92
|
-
|
|
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
|
-
.
|
|
96
|
-
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
.
|
|
111
|
-
|
|
298
|
+
.message-train-select-actions {
|
|
299
|
+
display: flex;
|
|
300
|
+
flex-wrap: wrap;
|
|
301
|
+
gap: var(--message-train-space-sm);
|
|
112
302
|
}
|
|
113
303
|
|
|
114
|
-
.
|
|
115
|
-
|
|
304
|
+
.message-train-message-actions {
|
|
305
|
+
float: right;
|
|
306
|
+
display: flex;
|
|
307
|
+
gap: var(--message-train-space-sm);
|
|
116
308
|
}
|
|
117
309
|
|
|
118
|
-
.
|
|
119
|
-
|
|
310
|
+
.message-train-mark-actions {
|
|
311
|
+
display: flex;
|
|
312
|
+
flex-wrap: wrap;
|
|
313
|
+
gap: var(--message-train-space-sm);
|
|
120
314
|
}
|
|
121
315
|
|
|
122
|
-
.
|
|
123
|
-
margin: 0
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
.
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
9
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|