pocket_phone 0.1.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 +7 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE +28 -0
- data/README.md +184 -0
- data/app/controllers/pocket_phone/api/sendblue_controller.rb +329 -0
- data/app/controllers/pocket_phone/application_controller.rb +33 -0
- data/app/controllers/pocket_phone/conversations_controller.rb +91 -0
- data/app/controllers/pocket_phone/media_controller.rb +14 -0
- data/app/controllers/pocket_phone/messages_controller.rb +65 -0
- data/app/controllers/pocket_phone/people_controller.rb +19 -0
- data/app/helpers/pocket_phone/application_helper.rb +145 -0
- data/app/views/layouts/pocket_phone/_script.html.erb +183 -0
- data/app/views/layouts/pocket_phone/_styles.html.erb +192 -0
- data/app/views/layouts/pocket_phone/application.html.erb +15 -0
- data/app/views/pocket_phone/conversations/_head.html.erb +44 -0
- data/app/views/pocket_phone/conversations/_message.html.erb +132 -0
- data/app/views/pocket_phone/conversations/_messages.html.erb +21 -0
- data/app/views/pocket_phone/conversations/_new_dialog.html.erb +52 -0
- data/app/views/pocket_phone/conversations/_sidebar.html.erb +28 -0
- data/app/views/pocket_phone/conversations/show.html.erb +67 -0
- data/config/routes.rb +41 -0
- data/lib/pocket_phone/carrier.rb +119 -0
- data/lib/pocket_phone/configuration.rb +60 -0
- data/lib/pocket_phone/database.rb +157 -0
- data/lib/pocket_phone/engine.rb +9 -0
- data/lib/pocket_phone/link_preview.rb +128 -0
- data/lib/pocket_phone/numbers.rb +40 -0
- data/lib/pocket_phone/payloads.rb +50 -0
- data/lib/pocket_phone/runner.rb +37 -0
- data/lib/pocket_phone/store.rb +106 -0
- data/lib/pocket_phone/version.rb +5 -0
- data/lib/pocket_phone/webhook.rb +68 -0
- data/lib/pocket_phone.rb +47 -0
- metadata +135 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
:root {
|
|
3
|
+
--bg: #ffffff; --side: #f4f4f6; --line: #e2e2e7; --text: #111113; --muted: #86868b;
|
|
4
|
+
--grey: #e9e9eb; --grey-text: #111113; --blue: #0b84fe; --green: #34c759; --red: #ff3b30;
|
|
5
|
+
--select: #0a6fe0; --card: #f1f1f3; --field: #ffffff; --shadow: 0 8px 40px rgba(0, 0, 0, .18);
|
|
6
|
+
color-scheme: light dark;
|
|
7
|
+
}
|
|
8
|
+
@media (prefers-color-scheme: dark) {
|
|
9
|
+
:root {
|
|
10
|
+
--bg: #1c1c1e; --side: #232326; --line: #38383c; --text: #f5f5f7; --muted: #98989f;
|
|
11
|
+
--grey: #3a3a3d; --grey-text: #f5f5f7; --blue: #0b84fe; --green: #30d158; --red: #ff453a;
|
|
12
|
+
--select: #0a6fe0; --card: #2c2c2f; --field: #1c1c1e; --shadow: 0 8px 40px rgba(0, 0, 0, .6);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
* { box-sizing: border-box; }
|
|
16
|
+
html, body { height: 100%; margin: 0; }
|
|
17
|
+
body {
|
|
18
|
+
background: var(--bg); color: var(--text);
|
|
19
|
+
font: 15px/1.35 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
20
|
+
-webkit-font-smoothing: antialiased;
|
|
21
|
+
}
|
|
22
|
+
button, input, select, textarea { font: inherit; color: inherit; }
|
|
23
|
+
code, pre { font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; }
|
|
24
|
+
[hidden] { display: none !important; }
|
|
25
|
+
|
|
26
|
+
.pp { display: grid; grid-template-columns: 320px minmax(0, 1fr); height: 100dvh; }
|
|
27
|
+
|
|
28
|
+
/* --- sidebar ------------------------------------------------------------ */
|
|
29
|
+
.pp-sidebar { display: flex; flex-direction: column; min-height: 0; background: var(--side); border-right: 1px solid var(--line); }
|
|
30
|
+
.pp-sidebar-top { display: flex; align-items: center; justify-content: space-between; padding: 16px 12px 10px 18px; }
|
|
31
|
+
.pp-sidebar-top h1 { margin: 0; font-size: 22px; font-weight: 700; letter-spacing: -.01em; }
|
|
32
|
+
#pp-sidebar { flex: 1; overflow-y: auto; padding: 0 8px; }
|
|
33
|
+
.pp-sidebar-empty { color: var(--muted); padding: 8px 12px; margin: 0; }
|
|
34
|
+
.pp-sidebar-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 10px 16px; border-top: 1px solid var(--line); color: var(--muted); font-size: 12px; }
|
|
35
|
+
.pp-sidebar-foot form { margin: 0; }
|
|
36
|
+
|
|
37
|
+
.pp-item { display: flex; align-items: center; gap: 10px; padding: 10px 10px 10px 4px; border-radius: 10px; color: inherit; text-decoration: none; }
|
|
38
|
+
.pp-item:hover { background: color-mix(in srgb, var(--text) 6%, transparent); }
|
|
39
|
+
.pp-item.current { background: var(--select); color: #fff; }
|
|
40
|
+
.pp-item-body { display: flex; flex-direction: column; min-width: 0; flex: 1; }
|
|
41
|
+
.pp-item-top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
|
|
42
|
+
.pp-item-name { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
43
|
+
.pp-item-time { font-size: 12px; color: var(--muted); flex: none; }
|
|
44
|
+
.pp-item-snippet { font-size: 13px; color: var(--muted); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; min-height: 1.35em; }
|
|
45
|
+
.pp-item.current .pp-item-time, .pp-item.current .pp-item-snippet { color: rgba(255, 255, 255, .8); }
|
|
46
|
+
.pp-unread { width: 8px; height: 8px; border-radius: 50%; flex: none; }
|
|
47
|
+
.pp-unread.on { background: var(--blue); }
|
|
48
|
+
|
|
49
|
+
.pp-avatar { display: inline-grid; place-items: center; flex: none; width: 44px; height: 44px; border-radius: 50%; overflow: hidden; color: #fff; font-size: 16px; font-weight: 600; background: linear-gradient(hsl(var(--hue, 220) 28% 68%), hsl(var(--hue, 220) 28% 50%)); }
|
|
50
|
+
.pp-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
51
|
+
|
|
52
|
+
/* --- thread ------------------------------------------------------------- */
|
|
53
|
+
.pp-thread { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
|
|
54
|
+
.pp-head { display: flex; align-items: center; gap: 10px; padding: 10px 16px; border-bottom: 1px solid var(--line); }
|
|
55
|
+
.pp-back { display: none; font-size: 30px; line-height: 1; color: var(--blue); text-decoration: none; padding: 0 6px 4px 0; }
|
|
56
|
+
.pp-head-title { display: flex; flex-direction: column; min-width: 0; flex: 1; }
|
|
57
|
+
.pp-head-title span { font-size: 12px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
58
|
+
.pp-head-controls { display: flex; align-items: center; gap: 10px; flex: none; }
|
|
59
|
+
.pp-head-controls form { display: flex; align-items: center; gap: 10px; margin: 0; }
|
|
60
|
+
.pp-head-controls label { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--muted); }
|
|
61
|
+
.pp-head-controls select { font-size: 13px; padding: 3px 6px; border-radius: 7px; border: 1px solid var(--line); background: var(--field); }
|
|
62
|
+
.pp-pill { font-size: 12px; padding: 3px 10px; border-radius: 999px; color: #fff; background: var(--green); }
|
|
63
|
+
.pp-pill.imessage { background: var(--blue); }
|
|
64
|
+
.pp-banner { padding: 8px 16px; font-size: 13px; background: color-mix(in srgb, #ffcc00 22%, var(--bg)); border-bottom: 1px solid var(--line); }
|
|
65
|
+
|
|
66
|
+
.pp-messages { flex: 1; overflow-y: auto; padding: 12px 16px 6px; display: flex; flex-direction: column; }
|
|
67
|
+
.pp-stamp { margin: 14px 0 6px; text-align: center; font-size: 12px; color: var(--muted); }
|
|
68
|
+
.pp-stamp strong { font-weight: 600; }
|
|
69
|
+
|
|
70
|
+
.pp-row { display: flex; flex-direction: column; align-items: flex-start; margin-top: 8px; }
|
|
71
|
+
.pp-row.continued { margin-top: 2px; }
|
|
72
|
+
.pp-row.mine { align-items: flex-end; }
|
|
73
|
+
.pp-sender { font-size: 12px; color: var(--muted); margin: 4px 0 2px 12px; }
|
|
74
|
+
.pp-row.mine .pp-sender { display: none; }
|
|
75
|
+
.pp-line { display: flex; align-items: center; gap: 6px; max-width: min(78%, 560px); }
|
|
76
|
+
.pp-row.mine .pp-line { flex-direction: row-reverse; }
|
|
77
|
+
.pp-stack { position: relative; display: flex; flex-direction: column; align-items: flex-start; gap: 3px; min-width: 0; }
|
|
78
|
+
.pp-row.mine .pp-stack { align-items: flex-end; }
|
|
79
|
+
|
|
80
|
+
.pp-bubble { padding: 7px 12px; border-radius: 18px; background: var(--grey); color: var(--grey-text); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
81
|
+
.pp-bubble a { color: inherit; }
|
|
82
|
+
.pp-row.mine .pp-bubble { background: var(--green); color: #fff; }
|
|
83
|
+
.pp-row.mine.imessage .pp-bubble { background: var(--blue); }
|
|
84
|
+
.pp-row.undelivered .pp-stack { opacity: .45; }
|
|
85
|
+
|
|
86
|
+
.pp-media { display: block; max-width: 280px; max-height: 320px; border-radius: 16px; }
|
|
87
|
+
.pp-audio { max-width: 280px; }
|
|
88
|
+
.pp-file { display: inline-block; padding: 8px 12px; border-radius: 14px; background: var(--card); color: inherit; text-decoration: none; }
|
|
89
|
+
.pp-carousel { display: flex; gap: 6px; max-width: 100%; overflow-x: auto; scroll-snap-type: x mandatory; }
|
|
90
|
+
.pp-carousel > * { flex: none; scroll-snap-align: start; }
|
|
91
|
+
.pp-carousel .pp-media { max-width: 200px; max-height: 240px; }
|
|
92
|
+
|
|
93
|
+
.pp-card { display: block; width: 280px; max-width: 100%; border-radius: 16px; overflow: hidden; background: var(--card); color: inherit; text-decoration: none; }
|
|
94
|
+
.pp-card-image { position: relative; }
|
|
95
|
+
.pp-card-image img { display: block; width: 100%; max-height: 220px; object-fit: cover; }
|
|
96
|
+
.pp-card-overlay { position: absolute; inset: auto 0 0 0; padding: 18px 12px 8px; color: #fff; background: linear-gradient(transparent, rgba(0, 0, 0, .6)); display: flex; flex-direction: column; }
|
|
97
|
+
.pp-card-body { display: flex; flex-direction: column; gap: 1px; padding: 8px 12px 9px; }
|
|
98
|
+
.pp-card-body strong { font-size: 14px; font-weight: 600; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
99
|
+
.pp-card-body span { font-size: 12px; color: var(--muted); }
|
|
100
|
+
.pp-card-split { flex-direction: row; justify-content: space-between; gap: 12px; }
|
|
101
|
+
.pp-card-split div { display: flex; flex-direction: column; }
|
|
102
|
+
.pp-card-split .trailing { text-align: right; }
|
|
103
|
+
.pp-card-app, .pp-card-warning { padding: 6px 12px 8px; font-size: 11px; color: var(--muted); border-top: 1px solid var(--line); }
|
|
104
|
+
.pp-card-warning { color: #b25000; background: color-mix(in srgb, #ffcc00 18%, var(--card)); }
|
|
105
|
+
|
|
106
|
+
.pp-reactions { position: absolute; top: -12px; right: -10px; display: flex; gap: 2px; padding: 2px 5px; border-radius: 999px; background: var(--card); border: 2px solid var(--bg); font-size: 12px; line-height: 1.2; }
|
|
107
|
+
.pp-row.mine .pp-reactions { right: auto; left: -10px; }
|
|
108
|
+
.pp-row:has(.pp-reactions) { margin-top: 18px; }
|
|
109
|
+
|
|
110
|
+
.pp-tools { display: flex; align-items: center; gap: 4px; flex: none; opacity: 0; transition: opacity .12s; }
|
|
111
|
+
.pp-row:hover .pp-tools, .pp-row.open .pp-tools { opacity: 1; }
|
|
112
|
+
.pp-tapbacks { display: flex; padding: 2px 4px; border-radius: 999px; background: var(--card); }
|
|
113
|
+
.pp-tapbacks button { border: 0; background: none; padding: 2px 3px; cursor: pointer; font-size: 14px; border-radius: 50%; }
|
|
114
|
+
.pp-tapbacks button:hover { transform: scale(1.25); }
|
|
115
|
+
.pp-info { border: 0; background: none; padding: 2px 4px; cursor: pointer; color: var(--muted); font-size: 15px; }
|
|
116
|
+
|
|
117
|
+
.pp-receipt, .pp-note { font-size: 11px; color: var(--muted); margin: 2px 6px 0; max-width: min(78%, 560px); }
|
|
118
|
+
.pp-receipt { font-weight: 600; }
|
|
119
|
+
.pp-receipt span { font-weight: 400; }
|
|
120
|
+
.pp-receipt.failed { color: var(--red); }
|
|
121
|
+
|
|
122
|
+
.pp-details { align-self: stretch; margin: 6px 0 4px; padding: 10px 12px; border-radius: 12px; background: var(--card); font-size: 12px; overflow-x: auto; }
|
|
123
|
+
.pp-details dl { display: grid; grid-template-columns: max-content 1fr; gap: 3px 12px; margin: 0; }
|
|
124
|
+
.pp-details dt { color: var(--muted); }
|
|
125
|
+
.pp-details dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
|
126
|
+
.pp-details ul { margin: 0 0 4px; padding: 0; list-style: none; }
|
|
127
|
+
.pp-details li span { color: var(--muted); }
|
|
128
|
+
.pp-details li.bad b, .pp-details li.bad { color: var(--red); }
|
|
129
|
+
.pp-details pre { margin: 8px 0 0; padding-top: 8px; border-top: 1px solid var(--line); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
130
|
+
|
|
131
|
+
.pp-typing { display: inline-flex; gap: 4px; padding: 11px 12px; }
|
|
132
|
+
.pp-typing i { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); animation: pp-blink 1.2s infinite both; }
|
|
133
|
+
.pp-typing i:nth-child(2) { animation-delay: .2s; }
|
|
134
|
+
.pp-typing i:nth-child(3) { animation-delay: .4s; }
|
|
135
|
+
@keyframes pp-blink { 0%, 60%, 100% { opacity: .35; } 30% { opacity: 1; } }
|
|
136
|
+
@media (prefers-reduced-motion: reduce) { .pp-typing i { animation: none; } }
|
|
137
|
+
|
|
138
|
+
/* --- compose ------------------------------------------------------------ */
|
|
139
|
+
.pp-compose { display: flex; align-items: flex-end; gap: 8px; padding: 8px 16px 14px; margin: 0; }
|
|
140
|
+
.pp-field { flex: 1; display: flex; align-items: flex-end; gap: 6px; min-width: 0; padding: 4px 4px 4px 12px; border: 1px solid var(--line); border-radius: 20px; background: var(--field); }
|
|
141
|
+
.pp-field textarea { flex: 1; min-width: 0; border: 0; outline: 0; resize: none; background: none; padding: 4px 0; max-height: 140px; }
|
|
142
|
+
.pp-as { border: 0; background: var(--card); border-radius: 999px; padding: 4px 8px; font-size: 12px; margin-bottom: 1px; max-width: 130px; }
|
|
143
|
+
.pp-attached { font-size: 12px; padding: 4px 8px; border-radius: 999px; background: var(--card); white-space: nowrap; max-width: 160px; overflow: hidden; text-overflow: ellipsis; cursor: pointer; }
|
|
144
|
+
.pp-send { display: grid; place-items: center; flex: none; width: 28px; height: 28px; border: 0; border-radius: 50%; background: var(--blue); color: #fff; cursor: pointer; }
|
|
145
|
+
.pp-compose.sms .pp-send { background: var(--green); }
|
|
146
|
+
.pp-icon { display: inline-grid; place-items: center; width: 34px; height: 34px; border: 0; border-radius: 50%; background: none; color: var(--blue); cursor: pointer; }
|
|
147
|
+
.pp-icon:hover { background: color-mix(in srgb, var(--text) 8%, transparent); }
|
|
148
|
+
.pp-attach { color: var(--muted); background: var(--card); margin-bottom: 2px; }
|
|
149
|
+
|
|
150
|
+
.pp-link { border: 0; background: none; padding: 0; color: var(--blue); cursor: pointer; font-size: inherit; }
|
|
151
|
+
.pp-button { border: 0; border-radius: 999px; padding: 8px 18px; background: var(--blue); color: #fff; font-weight: 600; cursor: pointer; }
|
|
152
|
+
.pp-hint { color: var(--muted); font-size: 13px; }
|
|
153
|
+
|
|
154
|
+
.pp-empty { margin: auto; max-width: 380px; padding: 24px; text-align: center; }
|
|
155
|
+
.pp-empty-phone { font-size: 56px; }
|
|
156
|
+
.pp-empty h2 { margin: 8px 0 6px; }
|
|
157
|
+
.pp-empty p { color: var(--muted); margin: 0 0 16px; }
|
|
158
|
+
.pp-empty .pp-hint { margin-top: 18px; }
|
|
159
|
+
|
|
160
|
+
/* --- new conversation --------------------------------------------------- */
|
|
161
|
+
.pp-dialog { width: min(440px, calc(100vw - 32px)); max-height: calc(100dvh - 48px); padding: 18px 20px 20px; border: 0; border-radius: 16px; background: var(--bg); color: var(--text); box-shadow: var(--shadow); }
|
|
162
|
+
.pp-dialog::backdrop { background: rgba(0, 0, 0, .35); }
|
|
163
|
+
.pp-dialog-top { display: flex; align-items: center; justify-content: space-between; }
|
|
164
|
+
.pp-dialog h2 { margin: 0; font-size: 18px; }
|
|
165
|
+
.pp-dialog h3 { margin: 18px 0 8px; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
|
|
166
|
+
.pp-people { display: flex; flex-direction: column; gap: 2px; }
|
|
167
|
+
.pp-people form { margin: 0; }
|
|
168
|
+
.pp-person { display: flex; align-items: center; gap: 10px; width: 100%; padding: 6px 8px; border: 0; border-radius: 10px; background: none; text-align: left; cursor: pointer; }
|
|
169
|
+
.pp-person:hover { background: var(--card); }
|
|
170
|
+
.pp-person span:last-child { display: flex; flex-direction: column; }
|
|
171
|
+
.pp-person small { color: var(--muted); }
|
|
172
|
+
.pp-form { display: flex; flex-direction: column; gap: 10px; margin: 0; }
|
|
173
|
+
.pp-form > label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--muted); }
|
|
174
|
+
.pp-form input[type="text"], .pp-form select { padding: 7px 10px; border: 1px solid var(--line); border-radius: 9px; background: var(--field); color: var(--text); }
|
|
175
|
+
.pp-form .pp-button { align-self: flex-start; }
|
|
176
|
+
.pp-checks { display: flex; flex-wrap: wrap; gap: 6px 16px; }
|
|
177
|
+
|
|
178
|
+
@media (max-width: 720px) {
|
|
179
|
+
.pp { grid-template-columns: minmax(0, 1fr); }
|
|
180
|
+
.pp.pp-has-thread .pp-sidebar, .pp:not(.pp-has-thread) .pp-thread { display: none; }
|
|
181
|
+
.pp:not(.pp-has-thread) .pp-sidebar { border-right: 0; }
|
|
182
|
+
.pp-back { display: block; }
|
|
183
|
+
.pp-head { flex-wrap: wrap; padding: 8px 12px; }
|
|
184
|
+
.pp-head-controls { width: 100%; justify-content: flex-end; }
|
|
185
|
+
.pp-messages { padding: 12px 12px 6px; }
|
|
186
|
+
.pp-compose { padding: 8px 12px 12px; }
|
|
187
|
+
.pp-line { max-width: 88%; }
|
|
188
|
+
.pp-tools { opacity: 1; }
|
|
189
|
+
.pp-tapbacks { display: none; }
|
|
190
|
+
.pp-row.open .pp-tapbacks { display: flex; }
|
|
191
|
+
}
|
|
192
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Pocket Phone</title>
|
|
7
|
+
<%= csrf_meta_tags %>
|
|
8
|
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📱</text></svg>">
|
|
9
|
+
<%= render "layouts/pocket_phone/styles" %>
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<%= yield %>
|
|
13
|
+
<%= render "layouts/pocket_phone/script" %>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<% group = @db.group?(@conversation) %>
|
|
2
|
+
<% line = @db.lines[@conversation["line"]] || {} %>
|
|
3
|
+
<% service = @db.service_for(@conversation) %>
|
|
4
|
+
<header class="pp-head" data-service="<%= service %>">
|
|
5
|
+
<a href="<%= root_path(list: 1) %>" class="pp-back" aria-label="Back">‹</a>
|
|
6
|
+
<%= pp_avatar(group ? "👥" : pp_title(@conversation), key: @conversation["id"], size: 36) %>
|
|
7
|
+
<div class="pp-head-title">
|
|
8
|
+
<strong><%= pp_title(@conversation) %></strong>
|
|
9
|
+
<span>
|
|
10
|
+
<% if group %>
|
|
11
|
+
<%= @conversation["participants"].map { |n| pp_person_name(n) }.join(", ") %>
|
|
12
|
+
<% else %>
|
|
13
|
+
<%= PocketPhone::Numbers.pretty(@conversation["number"]) %>
|
|
14
|
+
<% end %>
|
|
15
|
+
· texting <%= pp_line_name(@conversation["line"]) %>
|
|
16
|
+
<% if line["name"].present? %>(<%= PocketPhone::Numbers.pretty(@conversation["line"]) %>)<% end %>
|
|
17
|
+
</span>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div class="pp-head-controls">
|
|
21
|
+
<% if group %>
|
|
22
|
+
<span class="pp-pill <%= service.downcase %>"><%= service == "iMessage" ? "iMessage group" : "Group MMS" %></span>
|
|
23
|
+
<% else %>
|
|
24
|
+
<% person = @db.people[@conversation["number"]] || {} %>
|
|
25
|
+
<%= form_with url: people_path, method: :post, data: { remote: true } do %>
|
|
26
|
+
<%= hidden_field_tag :number, @conversation["number"] %>
|
|
27
|
+
<label>Phone
|
|
28
|
+
<%= select_tag :service, options_for_select([["iPhone (iMessage)", "iMessage"], ["Android (SMS)", "SMS"], ["Android (RCS)", "RCS"]], person["service"]), data: { autosubmit: true } %>
|
|
29
|
+
</label>
|
|
30
|
+
<label>Sends to it
|
|
31
|
+
<%= select_tag :outcome, options_for_select([["are delivered", "delivered"], ["fail after sending", "failed"], ["are refused", "rejected"]], person["outcome"]), data: { autosubmit: true } %>
|
|
32
|
+
</label>
|
|
33
|
+
<% end %>
|
|
34
|
+
<% end %>
|
|
35
|
+
<%= form_with url: delete_conversation_path(@conversation["id"]), method: :post, data: { confirm: "Delete this conversation?" } do %>
|
|
36
|
+
<button type="submit" class="pp-icon" title="Delete conversation" aria-label="Delete conversation">
|
|
37
|
+
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/></svg>
|
|
38
|
+
</button>
|
|
39
|
+
<% end %>
|
|
40
|
+
</div>
|
|
41
|
+
</header>
|
|
42
|
+
<% unless PocketPhone.config.webhook_url %>
|
|
43
|
+
<div class="pp-banner">No <code>webhook_url</code> is configured, so texts from this phone will not reach your app.</div>
|
|
44
|
+
<% end %>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<%# locals: message, group, last_phone, continued %>
|
|
2
|
+
<% phone = message["direction"] == "phone" %>
|
|
3
|
+
<% link = pp_preview_for(message) %>
|
|
4
|
+
<% receipt = phone ? pp_receipt(message, @conversation, last: last_phone) : nil %>
|
|
5
|
+
<% undelivered = !phone && message["status"] == "ERROR" %>
|
|
6
|
+
<% text = message["content"].to_s %>
|
|
7
|
+
<% only_link = link[:card] && text.strip == link[:url] %>
|
|
8
|
+
<% card = message["app_card"] %>
|
|
9
|
+
<div class="pp-row <%= "mine" if phone && !group %> <%= message["service"].to_s.downcase %> <%= "undelivered" if undelivered %> <%= "continued" if continued %>"
|
|
10
|
+
data-handle="<%= message["handle"] %>" data-from="<%= phone ? message["from_number"] : "line" %>">
|
|
11
|
+
<% if group && !continued %>
|
|
12
|
+
<div class="pp-sender"><%= pp_sender_name(message) %></div>
|
|
13
|
+
<% end %>
|
|
14
|
+
|
|
15
|
+
<div class="pp-line">
|
|
16
|
+
<div class="pp-stack">
|
|
17
|
+
<% if message["media_urls"].present? %>
|
|
18
|
+
<div class="pp-carousel">
|
|
19
|
+
<% message["media_urls"].each do |url| %><%= pp_media(url) %><% end %>
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% if message["media_url"].present? %>
|
|
23
|
+
<%= pp_media(message["media_url"]) %>
|
|
24
|
+
<% end %>
|
|
25
|
+
|
|
26
|
+
<% if card %>
|
|
27
|
+
<% layout = card["layout"] || {} %>
|
|
28
|
+
<div class="pp-card pp-appcard" title="<%= card["url"] %>">
|
|
29
|
+
<% if layout["imageUrl"].present? %>
|
|
30
|
+
<div class="pp-card-image">
|
|
31
|
+
<%= image_tag layout["imageUrl"], alt: "" %>
|
|
32
|
+
<% if layout["imageTitle"].present? || layout["imageSubtitle"].present? %>
|
|
33
|
+
<div class="pp-card-overlay"><strong><%= layout["imageTitle"] %></strong><span><%= layout["imageSubtitle"] %></span></div>
|
|
34
|
+
<% end %>
|
|
35
|
+
</div>
|
|
36
|
+
<% end %>
|
|
37
|
+
<div class="pp-card-body pp-card-split">
|
|
38
|
+
<div><strong><%= layout["caption"] %></strong><span><%= layout["subcaption"] %></span></div>
|
|
39
|
+
<div class="trailing"><strong><%= layout["trailingCaption"] %></strong><span><%= layout["trailingSubcaption"] %></span></div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="pp-card-app"><%= card["appName"] %> · app card, shown as it looks without the extension installed</div>
|
|
42
|
+
</div>
|
|
43
|
+
<% end %>
|
|
44
|
+
|
|
45
|
+
<% if text.present? && !only_link %>
|
|
46
|
+
<div class="pp-bubble"><%= pp_linkify(text) %></div>
|
|
47
|
+
<% end %>
|
|
48
|
+
|
|
49
|
+
<% if link[:card] %>
|
|
50
|
+
<% preview = link[:card] %>
|
|
51
|
+
<a class="pp-card" href="<%= preview["url"] %>" target="_blank" rel="noopener">
|
|
52
|
+
<% if preview["image"].present? %>
|
|
53
|
+
<div class="pp-card-image"><%= image_tag preview["image"], alt: "" %></div>
|
|
54
|
+
<% end %>
|
|
55
|
+
<div class="pp-card-body">
|
|
56
|
+
<strong><%= preview["title"].presence || preview["host"] %></strong>
|
|
57
|
+
<span><%= preview["host"] %></span>
|
|
58
|
+
</div>
|
|
59
|
+
<% if preview["local"] %>
|
|
60
|
+
<div class="pp-card-warning">A real phone could not load this preview: <%= preview["host"] %> is only reachable from your machine.</div>
|
|
61
|
+
<% end %>
|
|
62
|
+
</a>
|
|
63
|
+
<% end %>
|
|
64
|
+
|
|
65
|
+
<% if message["reactions"].present? %>
|
|
66
|
+
<div class="pp-reactions">
|
|
67
|
+
<% message["reactions"].each do |reaction| %>
|
|
68
|
+
<span title="<%= reaction["by"] == "line" ? pp_line_name(@conversation["line"]) : pp_person_name(reaction["by"]) %>"><%= pp_tapback(reaction["reaction"]) %></span>
|
|
69
|
+
<% end %>
|
|
70
|
+
</div>
|
|
71
|
+
<% end %>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="pp-tools">
|
|
75
|
+
<% if !phone && !undelivered && message["service"] == "iMessage" %>
|
|
76
|
+
<span class="pp-tapbacks">
|
|
77
|
+
<% PocketPhone::ApplicationHelper::TAPBACKS.each do |name, glyph| %>
|
|
78
|
+
<button type="button" data-tapback="<%= name %>" title="<%= name.capitalize %>"><%= glyph %></button>
|
|
79
|
+
<% end %>
|
|
80
|
+
</span>
|
|
81
|
+
<% end %>
|
|
82
|
+
<button type="button" class="pp-info" data-details title="Details" aria-label="Details">ⓘ</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<% if receipt %>
|
|
87
|
+
<div class="pp-receipt <%= "failed" if receipt[:failed] %>">
|
|
88
|
+
<%= receipt[:text] %><% if receipt[:detail] %> <span>· <%= receipt[:detail] %></span><% end %>
|
|
89
|
+
</div>
|
|
90
|
+
<% end %>
|
|
91
|
+
<% if undelivered %>
|
|
92
|
+
<div class="pp-receipt failed">Never reached this phone <span>· <%= message["error_message"] %></span></div>
|
|
93
|
+
<% end %>
|
|
94
|
+
<% if message["send_style"].present? %>
|
|
95
|
+
<div class="pp-note">Sent with <%= message["send_style"].to_s.tr("_", " ") %></div>
|
|
96
|
+
<% end %>
|
|
97
|
+
<% if link[:reason] %>
|
|
98
|
+
<div class="pp-note">No link preview: <%= link[:reason] %>.</div>
|
|
99
|
+
<% elsif link[:preview] && link[:preview]["error"] %>
|
|
100
|
+
<div class="pp-note">No link preview: the page could not be fetched (<%= link[:preview]["error"] %>).</div>
|
|
101
|
+
<% end %>
|
|
102
|
+
|
|
103
|
+
<div class="pp-details" hidden>
|
|
104
|
+
<dl>
|
|
105
|
+
<dt>Handle</dt><dd><code><%= message["handle"] %></code></dd>
|
|
106
|
+
<dt>Status</dt><dd><%= message["status"] %><% if message["error_code"] %> (<%= message["error_code"] %>)<% end %></dd>
|
|
107
|
+
<dt>Service</dt><dd><%= message["service"] %></dd>
|
|
108
|
+
<dt>From → to</dt><dd><%= message["from_number"] %> → <%= message["to_number"].presence || @conversation["group_id"] %></dd>
|
|
109
|
+
<dt>Sent</dt><dd><%= message["created_at"] %></dd>
|
|
110
|
+
<dt>Webhooks</dt>
|
|
111
|
+
<dd>
|
|
112
|
+
<% if message["webhooks"].blank? %>
|
|
113
|
+
none sent
|
|
114
|
+
<% else %>
|
|
115
|
+
<ul>
|
|
116
|
+
<% message["webhooks"].each do |hook| %>
|
|
117
|
+
<li class="<%= "bad" unless hook["status"].to_i.between?(200, 299) %>">
|
|
118
|
+
<b><%= hook["type"] %></b> → <%= pp_webhook_outcome(hook) %> <span><%= hook["url"] %></span>
|
|
119
|
+
</li>
|
|
120
|
+
<% end %>
|
|
121
|
+
</ul>
|
|
122
|
+
<% end %>
|
|
123
|
+
<% if phone %>
|
|
124
|
+
<button type="button" class="pp-link" data-redeliver>Send the webhook again</button>
|
|
125
|
+
<% end %>
|
|
126
|
+
</dd>
|
|
127
|
+
</dl>
|
|
128
|
+
<% if message["request"].present? %>
|
|
129
|
+
<pre><%= pp_json(message["request"]) %></pre>
|
|
130
|
+
<% end %>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<% messages = @db.messages_for(@conversation) %>
|
|
2
|
+
<% group = @db.group?(@conversation) %>
|
|
3
|
+
<% last_phone = messages.reverse.find { |m| m["direction"] == "phone" } %>
|
|
4
|
+
<% if messages.empty? %>
|
|
5
|
+
<p class="pp-stamp">No messages yet. Say something.</p>
|
|
6
|
+
<% end %>
|
|
7
|
+
<% previous = nil %>
|
|
8
|
+
<% messages.each do |message| %>
|
|
9
|
+
<% if pp_gap?(previous, message) %>
|
|
10
|
+
<p class="pp-stamp"><%= pp_stamp(message["created_at"]) %></p>
|
|
11
|
+
<% end %>
|
|
12
|
+
<%= render "pocket_phone/conversations/message", message: message, group: group,
|
|
13
|
+
last_phone: last_phone.equal?(message),
|
|
14
|
+
continued: previous && !pp_gap?(previous, message) && previous["from_number"] == message["from_number"] %>
|
|
15
|
+
<% previous = message %>
|
|
16
|
+
<% end %>
|
|
17
|
+
<% if @db.typing?(@conversation) %>
|
|
18
|
+
<div class="pp-row" data-from="line">
|
|
19
|
+
<div class="pp-bubble pp-typing" aria-label="Typing"><i></i><i></i><i></i></div>
|
|
20
|
+
</div>
|
|
21
|
+
<% end %>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<% people = @db.people.values %>
|
|
2
|
+
<dialog class="pp-dialog" data-dialog>
|
|
3
|
+
<div class="pp-dialog-top">
|
|
4
|
+
<h2>New conversation</h2>
|
|
5
|
+
<button type="button" class="pp-icon" data-close-dialog aria-label="Close">✕</button>
|
|
6
|
+
</div>
|
|
7
|
+
|
|
8
|
+
<% if people.any? %>
|
|
9
|
+
<section>
|
|
10
|
+
<h3>Text as someone you already have</h3>
|
|
11
|
+
<div class="pp-people">
|
|
12
|
+
<% people.each do |person| %>
|
|
13
|
+
<%= form_with url: conversations_path, method: :post do %>
|
|
14
|
+
<%= hidden_field_tag :number, person["number"] %>
|
|
15
|
+
<button type="submit" class="pp-person">
|
|
16
|
+
<%= pp_avatar(pp_person_name(person["number"]), key: person["number"], size: 28) %>
|
|
17
|
+
<span><strong><%= pp_person_name(person["number"]) %></strong><small><%= PocketPhone::Numbers.pretty(person["number"]) %> · <%= person["service"] %></small></span>
|
|
18
|
+
</button>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
</section>
|
|
23
|
+
<% end %>
|
|
24
|
+
|
|
25
|
+
<section>
|
|
26
|
+
<h3>A new person</h3>
|
|
27
|
+
<%= form_with url: conversations_path, method: :post, class: "pp-form" do %>
|
|
28
|
+
<label>Name <%= text_field_tag :name, nil, placeholder: "Alice", autocomplete: "off" %></label>
|
|
29
|
+
<label>Number <%= text_field_tag :number, PocketPhone::Numbers.random, required: true, autocomplete: "off" %></label>
|
|
30
|
+
<label>Phone <%= select_tag :service, options_for_select([["iPhone (iMessage)", "iMessage"], ["Android (SMS)", "SMS"], ["Android (RCS)", "RCS"]]) %></label>
|
|
31
|
+
<button type="submit" class="pp-button">Start texting</button>
|
|
32
|
+
<% end %>
|
|
33
|
+
</section>
|
|
34
|
+
|
|
35
|
+
<% if people.size >= 2 %>
|
|
36
|
+
<section>
|
|
37
|
+
<h3>A group</h3>
|
|
38
|
+
<%= form_with url: conversations_path, method: :post, class: "pp-form" do %>
|
|
39
|
+
<%= hidden_field_tag :group, 1 %>
|
|
40
|
+
<label>Group name <%= text_field_tag :group_name, nil, placeholder: "Optional", autocomplete: "off" %></label>
|
|
41
|
+
<div class="pp-checks">
|
|
42
|
+
<% people.each do |person| %>
|
|
43
|
+
<label><%= check_box_tag "participants[]", person["number"], false, id: nil %> <%= pp_person_name(person["number"]) %></label>
|
|
44
|
+
<% end %>
|
|
45
|
+
</div>
|
|
46
|
+
<button type="submit" class="pp-button">Start group</button>
|
|
47
|
+
<% end %>
|
|
48
|
+
</section>
|
|
49
|
+
<% else %>
|
|
50
|
+
<p class="pp-hint">Add two people and you can put them in a group chat.</p>
|
|
51
|
+
<% end %>
|
|
52
|
+
</dialog>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<% conversations = @db.sorted_conversations %>
|
|
2
|
+
<% if conversations.empty? %>
|
|
3
|
+
<p class="pp-sidebar-empty">Nothing here yet.</p>
|
|
4
|
+
<% end %>
|
|
5
|
+
<% conversations.each do |conversation| %>
|
|
6
|
+
<% last = @db.messages_for(conversation).last %>
|
|
7
|
+
<% current = @conversation && @conversation["id"] == conversation["id"] %>
|
|
8
|
+
<% title = pp_title(conversation) %>
|
|
9
|
+
<a href="<%= conversation_path(conversation["id"]) %>" class="pp-item <%= "current" if current %>">
|
|
10
|
+
<span class="pp-unread <%= "on" if conversation["unread"].to_i.positive? && !current %>"></span>
|
|
11
|
+
<%= pp_avatar(@db.group?(conversation) ? "👥" : title, key: conversation["id"]) %>
|
|
12
|
+
<span class="pp-item-body">
|
|
13
|
+
<span class="pp-item-top">
|
|
14
|
+
<span class="pp-item-name"><%= title %></span>
|
|
15
|
+
<span class="pp-item-time"><%= pp_clock(last ? last["created_at"] : conversation["updated_at"]) %></span>
|
|
16
|
+
</span>
|
|
17
|
+
<span class="pp-item-snippet">
|
|
18
|
+
<% if @db.typing?(conversation) %>
|
|
19
|
+
<em>typing…</em>
|
|
20
|
+
<% elsif last %>
|
|
21
|
+
<%= pp_snippet(last) %>
|
|
22
|
+
<% else %>
|
|
23
|
+
<em>No messages yet</em>
|
|
24
|
+
<% end %>
|
|
25
|
+
</span>
|
|
26
|
+
</span>
|
|
27
|
+
</a>
|
|
28
|
+
<% end %>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<% group = @conversation && @db.group?(@conversation) %>
|
|
2
|
+
<div class="pp <%= "pp-has-thread" if @conversation %>" data-pp
|
|
3
|
+
data-base="<%= pp_base %>"
|
|
4
|
+
data-conversation="<%= @conversation && @conversation["id"] %>"
|
|
5
|
+
data-group="<%= group ? 1 : 0 %>">
|
|
6
|
+
<aside class="pp-sidebar">
|
|
7
|
+
<div class="pp-sidebar-top">
|
|
8
|
+
<h1>Messages</h1>
|
|
9
|
+
<button type="button" class="pp-icon" data-open-dialog title="New conversation" aria-label="New conversation">
|
|
10
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>
|
|
11
|
+
</button>
|
|
12
|
+
</div>
|
|
13
|
+
<div id="pp-sidebar"><%= render "pocket_phone/conversations/sidebar" %></div>
|
|
14
|
+
<div class="pp-sidebar-foot">
|
|
15
|
+
<%= form_with url: clear_path, method: :post, data: { confirm: "Clear every conversation?" } do %>
|
|
16
|
+
<button type="submit" class="pp-link">Clear conversations</button>
|
|
17
|
+
<% end %>
|
|
18
|
+
<span>pocket_phone <%= PocketPhone::VERSION %></span>
|
|
19
|
+
</div>
|
|
20
|
+
</aside>
|
|
21
|
+
|
|
22
|
+
<main class="pp-thread">
|
|
23
|
+
<% if @conversation %>
|
|
24
|
+
<div id="pp-head"><%= render "pocket_phone/conversations/head" %></div>
|
|
25
|
+
<div id="pp-messages" class="pp-messages"><%= render "pocket_phone/conversations/messages" %></div>
|
|
26
|
+
|
|
27
|
+
<%= form_with url: conversation_messages_path(@conversation["id"]), method: :post, multipart: true,
|
|
28
|
+
class: "pp-compose", data: { compose: true } do %>
|
|
29
|
+
<label class="pp-icon pp-attach" title="Attach a file">
|
|
30
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>
|
|
31
|
+
<input type="file" name="file" hidden>
|
|
32
|
+
</label>
|
|
33
|
+
<div class="pp-field">
|
|
34
|
+
<% if group %>
|
|
35
|
+
<select name="as" class="pp-as" data-persona title="Who is texting">
|
|
36
|
+
<% @conversation["participants"].each do |number| %>
|
|
37
|
+
<option value="<%= number %>"><%= pp_person_name(number) %></option>
|
|
38
|
+
<% end %>
|
|
39
|
+
</select>
|
|
40
|
+
<% end %>
|
|
41
|
+
<span class="pp-attached" data-attached hidden></span>
|
|
42
|
+
<textarea name="content" rows="1" placeholder="<%= @db.service_for(@conversation) == "iMessage" ? "iMessage" : "Text Message" %>" autofocus></textarea>
|
|
43
|
+
<button type="submit" class="pp-send" aria-label="Send">
|
|
44
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19V5M5 12l7-7 7 7"/></svg>
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
<% end %>
|
|
48
|
+
<% else %>
|
|
49
|
+
<div class="pp-empty">
|
|
50
|
+
<div class="pp-empty-phone">📱</div>
|
|
51
|
+
<% if @db.conversations.any? %>
|
|
52
|
+
<h2>Pick a conversation</h2>
|
|
53
|
+
<p>Or start a new one.</p>
|
|
54
|
+
<% else %>
|
|
55
|
+
<h2>No conversations yet</h2>
|
|
56
|
+
<p>Messages your app sends through Sendblue will show up here. Or pick up the phone and text it first.</p>
|
|
57
|
+
<% end %>
|
|
58
|
+
<button type="button" class="pp-button" data-open-dialog>New conversation</button>
|
|
59
|
+
<% unless PocketPhone.config.webhook_url %>
|
|
60
|
+
<p class="pp-hint">No <code>webhook_url</code> is configured, so texts from this phone will not reach your app. See the README.</p>
|
|
61
|
+
<% end %>
|
|
62
|
+
</div>
|
|
63
|
+
<% end %>
|
|
64
|
+
</main>
|
|
65
|
+
|
|
66
|
+
<%= render "pocket_phone/conversations/new_dialog" %>
|
|
67
|
+
</div>
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
PocketPhone::Engine.routes.draw do
|
|
4
|
+
# --- the phone ------------------------------------------------------------
|
|
5
|
+
root to: "conversations#index"
|
|
6
|
+
get "state" => "conversations#state"
|
|
7
|
+
post "clear" => "conversations#clear", as: :clear
|
|
8
|
+
post "conversations" => "conversations#create", as: :conversations
|
|
9
|
+
get "conversations/:id" => "conversations#show", as: :conversation
|
|
10
|
+
post "conversations/:id/delete" => "conversations#destroy", as: :delete_conversation
|
|
11
|
+
post "conversations/:id/typing" => "conversations#typing"
|
|
12
|
+
post "conversations/:id/messages" => "messages#create", as: :conversation_messages
|
|
13
|
+
post "messages/:handle/reactions" => "messages#react"
|
|
14
|
+
post "messages/:handle/redeliver" => "messages#redeliver"
|
|
15
|
+
post "people" => "people#update", as: :people
|
|
16
|
+
get "media/:name" => "media#show", as: :media, constraints: { name: %r{[^/]+} }
|
|
17
|
+
|
|
18
|
+
# --- the fake Sendblue API ------------------------------------------------
|
|
19
|
+
# Point your Sendblue client's base URL at wherever this engine is mounted
|
|
20
|
+
# and these answer in place of https://api.sendblue.com.
|
|
21
|
+
scope module: :api, defaults: { format: :json } do
|
|
22
|
+
post "api/send-message" => "sendblue#send_message"
|
|
23
|
+
post "api/send-group-message" => "sendblue#send_group_message"
|
|
24
|
+
post "api/send-carousel" => "sendblue#send_carousel"
|
|
25
|
+
post "api/send-typing-indicator" => "sendblue#send_typing_indicator"
|
|
26
|
+
post "api/send-reaction" => "sendblue#send_reaction"
|
|
27
|
+
post "api/mark-read" => "sendblue#mark_read"
|
|
28
|
+
post "api/modify-group" => "sendblue#modify_group"
|
|
29
|
+
get "api/evaluate-service" => "sendblue#evaluate_service"
|
|
30
|
+
post "api/upload-file" => "sendblue#upload_file"
|
|
31
|
+
post "api/upload-media-object" => "sendblue#upload_media_object"
|
|
32
|
+
post "api/v2/contact-sharing/profile" => "sendblue#contact_profile"
|
|
33
|
+
post "api/v2/contact-profile" => "sendblue#contact_profile"
|
|
34
|
+
get "api/v2/messages" => "sendblue#messages"
|
|
35
|
+
get "api/v2/messages/:handle" => "sendblue#message"
|
|
36
|
+
delete "api/v2/messages/:handle" => "sendblue#delete_message"
|
|
37
|
+
delete "api/message/:handle" => "sendblue#delete_message"
|
|
38
|
+
match "api/*path" => "sendblue#missing", via: :all
|
|
39
|
+
match "accounts/*path" => "sendblue#missing", via: :all
|
|
40
|
+
end
|
|
41
|
+
end
|