Compare commits
40
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b9b7a2671 | ||
|
|
1a3362f1cd | ||
|
|
9009667c5a | ||
|
|
a59023575b | ||
|
|
b2b1ca2934 | ||
|
|
afd1bbb58a | ||
|
|
de655a105a | ||
|
|
2c1c0b2544 | ||
|
|
ccf5318e4c | ||
|
|
7322abfd55 | ||
|
|
09518bbac2 | ||
|
|
b6a831ec91 | ||
|
|
378cd622c4 | ||
|
|
939e3e59cf | ||
|
|
22e82b1c7f | ||
|
|
e495917356 | ||
|
|
cf4b58cab2 | ||
|
|
0a96014484 | ||
|
|
3e8fd9d6e6 | ||
|
|
a06d29863d | ||
|
|
de24e23127 | ||
|
|
9417e1aa49 | ||
|
|
1f8d26a215 | ||
|
|
e4a0b60f1e | ||
|
|
19612e4236 | ||
|
|
af1e9c609b | ||
|
|
5d18c71475 | ||
|
|
20fc9ae343 | ||
|
|
2e8b9145b7 | ||
|
|
859dec8410 | ||
|
|
8f13d2c856 | ||
|
|
d2a226852b | ||
|
|
9c7a05f616 | ||
|
|
a6e59936e2 | ||
|
|
e3d95eafe1 | ||
|
|
e30568ecd5 | ||
|
|
9ac8ea8f93 | ||
|
|
1bac018961 | ||
|
|
1aba91d6f3 | ||
|
|
fd750558ac |
@@ -1 +0,0 @@
|
|||||||
tinode.js
|
|
||||||
@@ -4,9 +4,9 @@ Example using [Tinode](https://github.com/tinode/chat/) to build a single-page c
|
|||||||
[React](https://facebook.github.io/react/), Google's material design [fonts](https://www.google.com/fonts/)
|
[React](https://facebook.github.io/react/), Google's material design [fonts](https://www.google.com/fonts/)
|
||||||
and [icons](https://google.github.io/material-design-icons/#icon-font-for-the-web). The [Tinode javascript library](https://github.com/tinode/tinode-js/) has no external dependencies. Overall it's a lot like open source WhatsApp or Telegram web apps.
|
and [icons](https://google.github.io/material-design-icons/#icon-font-for-the-web). The [Tinode javascript library](https://github.com/tinode/tinode-js/) has no external dependencies. Overall it's a lot like open source WhatsApp or Telegram web apps.
|
||||||
|
|
||||||
This is work in progress. Bugs are expected. The app was tested in the latest Chrome & Firefox only.
|
Although the app is generally usable, keep in mind that this is work in progress. Some bugs probably exist. The app was tested in the latest Chrome & Firefox only.
|
||||||
|
|
||||||
Try it (possibly older version) live at http://api.tinode.co/x/example-react-js/. Login as one of `alice`, `bob`, `carol`, `dave`, `frank`. Password is `<login>123`, e.g. password for `alice` is `alice123`. You can discover other users by email or phone by prefixing them with `email:` or `tel:` respectively. Emails are `<login>@example.com`, e.g. `alice@example.com`, phones are `17025550001` through `17025550009`. The demo is reset every night at 2:15 am Pacific time.
|
Try possibly newer or older version live at https://api.tinode.co/x/example-react-js/. Login as one of `alice`, `bob`, `carol`, `dave`, `frank`. Password is `<login>123`, e.g. password for `alice` is `alice123`. You can discover other users by email or phone by prefixing them with `email:` or `tel:` respectively. Emails are `<login>@example.com`, e.g. `alice@example.com`, phones are `17025550001` through `17025550009`. The demo is reset every night at 2:15am Pacific time.
|
||||||
|
|
||||||
License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
License: [Apache 2.0](http://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
|
||||||
@@ -16,24 +16,23 @@ Background pattern from http://subtlepatterns.com/, commercial and non-commercia
|
|||||||
|
|
||||||
## Missing functionality
|
## Missing functionality
|
||||||
|
|
||||||
* Cannot send images or other media.
|
* Previews not generated for videos, audio, links or docs.
|
||||||
* Cannot delete messages.
|
* No indication of some chat statuses (muted, blocked, etc.).
|
||||||
* No typing indicators.
|
* Browser push notifications are not implemented.
|
||||||
* No indication of chat status (muted, blocked, etc.).
|
* No end-to-end encryption.
|
||||||
* Browser push notifications are missing.
|
|
||||||
* No encryption.
|
|
||||||
* Editing chat permissions is incomplete.
|
* Editing chat permissions is incomplete.
|
||||||
|
* Emoji support is weak.
|
||||||
|
|
||||||
## Responsive design
|
## Responsive design
|
||||||
|
|
||||||
### Desktop screenshot
|
### Desktop screenshot
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="web-desktop-1.png" alt="Desktop web: full app" width=866 />
|
<img src="web-desktop-2.png" alt="Desktop web: full app" width=866 />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
### Mobile screenshots
|
### Mobile screenshots
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="web-mob-contacts-1.png" alt="Mobile web: contacts" width=323 /> <img src="web-mob-chat-1.png" alt="Mobile web: chat" width=323 /> <img src="web-mob-info-1.png" alt="Mobile web: topic info" width=323 /> <img src="web-mob-new-chat-1.png" alt="Mobile web: start new chat" width=323 />
|
<kbd><img src="web-mob-contacts-1.png" alt="Mobile web: contacts" width=323 /></kbd> <kbd><img src="web-mob-chat-1.png" alt="Mobile web: chat" width=323 /></kbd> <kbd><img src="web-mob-info-1.png" alt="Mobile web: topic info" width=323 /></kbd> <kbd><img src="web-mob-new-chat-1.png" alt="Mobile web: start new chat" width=323 /></kbd>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
+213
-36
@@ -247,6 +247,36 @@ ul.menu > li.separator {
|
|||||||
ul.menu > li.separator:hover {
|
ul.menu > li.separator:hover {
|
||||||
background-color: #ccc;
|
background-color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The down arrow head - trigger for context menu */
|
||||||
|
li .menuTrigger {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.25rem;
|
||||||
|
right: 0.25rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
line-height: 1.2rem;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
li:hover .menuTrigger {
|
||||||
|
opacity: 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:hover .menuTrigger a {
|
||||||
|
color: #757575;
|
||||||
|
}
|
||||||
|
|
||||||
|
li.left:hover .menuTrigger {
|
||||||
|
-text-shadow: #c5e1a5 0 0 15px, #c5e1a5 0 0 5px;
|
||||||
|
background-image: radial-gradient(#c5e1a5 40%, transparent 75%);
|
||||||
|
}
|
||||||
|
li.right:hover .menuTrigger {
|
||||||
|
-text-shadow: #fafafa 0 0 15px, #fafafa 0 0 5px;
|
||||||
|
background-image: radial-gradient(#fafafa 40%, transparent 75%);
|
||||||
|
}
|
||||||
|
|
||||||
/* END popup/dropdown menu */
|
/* END popup/dropdown menu */
|
||||||
|
|
||||||
/* Generic text colors */
|
/* Generic text colors */
|
||||||
@@ -258,6 +288,14 @@ ul.menu > li.separator:hover {
|
|||||||
color: #D32F2F; /* red 700 */
|
color: #D32F2F; /* red 700 */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gray {
|
||||||
|
color: #757575;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
font-size: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
.hr {
|
.hr {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -411,7 +449,6 @@ table.permission-editor td.checkbox {
|
|||||||
.permission-editor .material-icons {
|
.permission-editor .material-icons {
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
line-height: 1.1rem;
|
line-height: 1.1rem;
|
||||||
vertical-align: middle;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-icons.clickable {
|
.material-icons.clickable {
|
||||||
@@ -505,17 +542,51 @@ table.permission-editor td.checkbox {
|
|||||||
|
|
||||||
|
|
||||||
/* Area which displays an error message */
|
/* Area which displays an error message */
|
||||||
#alert-box {
|
.alert-box {
|
||||||
background-color: transparent;
|
position: relative;
|
||||||
height: 0;
|
display: none;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding: 1rem 1.5rem 1rem 0.5rem;
|
||||||
}
|
}
|
||||||
#alert-box.error-alert {
|
.alert-box .cancel {
|
||||||
padding: 0.5rem;
|
position: absolute;
|
||||||
|
right: 0.25rem;
|
||||||
|
top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alert-box.error {
|
||||||
|
display: flex;
|
||||||
background-color: #ffcdd2;
|
background-color: #ffcdd2;
|
||||||
border-color: #d32f2f;
|
border-bottom: 1px solid #ffbfc8;
|
||||||
color: #b71c1c;
|
color: #b71c1c;
|
||||||
height: auto;
|
}
|
||||||
|
.alert-box.warning {
|
||||||
|
display: flex;
|
||||||
|
background-color: #fff9c4;
|
||||||
|
border-bottom: 1px solid #fff59d;
|
||||||
|
color: #607d8b;
|
||||||
|
}
|
||||||
|
.alert-box .cancel .material-icons {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.alert-box.error .cancel .material-icons {
|
||||||
|
color: #b71c1c;
|
||||||
|
}
|
||||||
|
.alert-box.warning .cancel .material-icons {
|
||||||
|
color: #757575;
|
||||||
|
}
|
||||||
|
.alert-box .icon {
|
||||||
|
display: block;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
.alert-box .icon .material-icons {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.alert-box.warning .icon .material-icons {
|
||||||
|
color: #fbc02d;
|
||||||
|
}
|
||||||
|
.alert-box.error .icon .material-icons {
|
||||||
|
color: #ef5350;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* List of contacts */
|
/* List of contacts */
|
||||||
@@ -540,6 +611,7 @@ table.permission-editor td.checkbox {
|
|||||||
|
|
||||||
.contact-box > li {
|
.contact-box > li {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
padding: 0.35rem 0.25rem 0.5rem 0.35rem;
|
padding: 0.35rem 0.25rem 0.5rem 0.35rem;
|
||||||
min-height: 3.5rem;
|
min-height: 3.5rem;
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
@@ -571,20 +643,6 @@ table.permission-editor td.checkbox {
|
|||||||
font-weight: lighter;
|
font-weight: lighter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The down arrow head - trigger for context menu in contacts list */
|
|
||||||
.contact-box li .menuTrigger {
|
|
||||||
position: relative;
|
|
||||||
margin-left: auto;
|
|
||||||
width: 1.5rem;
|
|
||||||
height: 1.5rem;
|
|
||||||
}
|
|
||||||
.contact-box li .menuTrigger a {
|
|
||||||
color: transparent;
|
|
||||||
}
|
|
||||||
.contact-box li:hover .menuTrigger a {
|
|
||||||
color: #757575;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Avatar and online-offline indicator */
|
/* Avatar and online-offline indicator */
|
||||||
.contact-box .avatar-box {
|
.contact-box .avatar-box {
|
||||||
margin-right: 0.35rem;
|
margin-right: 0.35rem;
|
||||||
@@ -609,7 +667,7 @@ table.permission-editor td.checkbox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Online-offline indicator positioned inside avatar-box */
|
/* Online-offline indicator positioned inside avatar-box */
|
||||||
.contact-box .online, .offline {
|
.avatar-box .online, .offline {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 0.75rem;
|
width: 0.75rem;
|
||||||
min-width: 0.75rem;
|
min-width: 0.75rem;
|
||||||
@@ -621,20 +679,37 @@ table.permission-editor td.checkbox {
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0.1rem;
|
bottom: 0.1rem;
|
||||||
}
|
}
|
||||||
.contact-box .online {
|
.avatar-box .online {
|
||||||
background-color: #4caf50;
|
background-color: #4caf50;
|
||||||
}
|
}
|
||||||
.contact-box .offline {
|
.avatar-box .offline {
|
||||||
background-color: #C0C0C0;
|
background-color: #C0C0C0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Typing indicator: animation of the online dot */
|
||||||
|
.online.typing {
|
||||||
|
animation: typing 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes typing {
|
||||||
|
0% { background-color: #40C040; }
|
||||||
|
25% { background-image: radial-gradient(#30D030 0%, #40C040 67%); }
|
||||||
|
33.3% { background-image: radial-gradient(#10EE10 0%, #40C040 50%); }
|
||||||
|
41.7% { background-image: radial-gradient(#00FF00 0%, #40C040 35%); }
|
||||||
|
50% { background-image: radial-gradient(#30D030 0%, #40C040 67%); }
|
||||||
|
58.3% { background-image: radial-gradient(#10EE10 0%, #40C040 50%); }
|
||||||
|
66.7% { background-image: radial-gradient(#00FF00 0%, #40C040 35%); }
|
||||||
|
75% { background-image: radial-gradient(#30D030 0%, #40C040 67%); }
|
||||||
|
83.3% { background-image: radial-gradient(#10EE10 0%, #40C040 50%); }
|
||||||
|
91.6% { background-image: radial-gradient(#00FF00 0%, #40C040 35%); }
|
||||||
|
}
|
||||||
|
|
||||||
.contact-box .checkmark.material-icons {
|
.contact-box .checkmark.material-icons {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: 1.5rem;
|
line-height: 1.5rem;
|
||||||
vertical-align: middle;
|
|
||||||
color: #0097A7;
|
color: #0097A7;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0.05rem;
|
right: 0.05rem;
|
||||||
@@ -772,6 +847,10 @@ table.permission-editor td.checkbox {
|
|||||||
/* END of sidepanel styles */
|
/* END of sidepanel styles */
|
||||||
|
|
||||||
/* Topic/messages view */
|
/* Topic/messages view */
|
||||||
|
#topic-view {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
#topic-caption-panel {
|
#topic-caption-panel {
|
||||||
background-color: #b0bec5;
|
background-color: #b0bec5;
|
||||||
}
|
}
|
||||||
@@ -784,7 +863,6 @@ table.permission-editor td.checkbox {
|
|||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#topic-caption-panel .avatar-box {
|
#topic-caption-panel .avatar-box {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 3rem;
|
width: 3rem;
|
||||||
@@ -806,6 +884,7 @@ table.permission-editor td.checkbox {
|
|||||||
|
|
||||||
/* Scrollable panel where the messages are shown */
|
/* Scrollable panel where the messages are shown */
|
||||||
#messages-panel {
|
#messages-panel {
|
||||||
|
position: relative;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
background-color: #eceff1;
|
background-color: #eceff1;
|
||||||
@@ -813,12 +892,37 @@ table.permission-editor td.checkbox {
|
|||||||
background-image: url("../img/ignasi_pattern_s.png");
|
background-image: url("../img/ignasi_pattern_s.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#messages-panel.write-only::after {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top:0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-image: repeating-linear-gradient(45deg,
|
||||||
|
transparent, transparent 0.5rem, rgba(255,255,255,.5) 0.5rem, rgba(255,255,255,.5) 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
#write-only-note {
|
||||||
|
position: absolute;
|
||||||
|
background-color: white;
|
||||||
|
color: #666;
|
||||||
|
height: auto;
|
||||||
|
width: auto;
|
||||||
|
line-height: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
padding: 0.25rem 1.5rem;
|
||||||
|
}
|
||||||
/* Footer-panel with the input for the new message */
|
/* Footer-panel with the input for the new message */
|
||||||
#send-message-panel {
|
#send-message-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: none;
|
flex: none;
|
||||||
padding: 0.4rem;
|
padding: 0.4rem 0.4rem 0.4rem 0.6rem;
|
||||||
background-color: #eceff1;
|
background-color: #eceff1;
|
||||||
box-shadow: 0 -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
|
box-shadow: 0 -1px rgba(0,0,0,0.1); /* Faint shadow above panel */
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
@@ -829,6 +933,18 @@ table.permission-editor td.checkbox {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#send-message-panel .material-icons {
|
||||||
|
min-width: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#send-message-panel .material-icons.secondary, .material-icons.disabled {
|
||||||
|
color: #999!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#send-message-panel .material-icons.secondary:hover {
|
||||||
|
color: #2196f3!important;
|
||||||
|
}
|
||||||
|
|
||||||
/* A static panel shown when no topic is selected */
|
/* A static panel shown when no topic is selected */
|
||||||
#dummy-view {
|
#dummy-view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -845,6 +961,49 @@ table.permission-editor td.checkbox {
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#image-preview {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 10;
|
||||||
|
background-color: rgba(255,255,255,0.90);
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-preview-caption-panel {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
background-color: transparent;
|
||||||
|
line-height: 2.5rem;
|
||||||
|
height: 2.5rem;
|
||||||
|
padding: 1rem 0.75rem 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-preview-caption-panel a {
|
||||||
|
color: #757575;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-preview-caption-panel a:hover {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-preview-container {
|
||||||
|
margin: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#image-preview-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
height: 3.2rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
/* Talk bubbles */
|
/* Talk bubbles */
|
||||||
.chat-box {
|
.chat-box {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
@@ -869,6 +1028,7 @@ table.permission-editor td.checkbox {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-box .bubble {
|
.chat-box .bubble {
|
||||||
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 40rem;
|
max-width: 40rem;
|
||||||
min-width: 12rem;
|
min-width: 12rem;
|
||||||
@@ -980,7 +1140,7 @@ table.permission-editor td.checkbox {
|
|||||||
|
|
||||||
.chat-box .timestamp {
|
.chat-box .timestamp {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position:relative;
|
position: relative;
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
color: #777;
|
color: #777;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
@@ -1009,6 +1169,24 @@ table.permission-editor td.checkbox {
|
|||||||
color:#777;
|
color:#777;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Inline image styles */
|
||||||
|
.inline-image {
|
||||||
|
max-width: 36rem;
|
||||||
|
max-height: 24rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 0.1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Attachment styles */
|
||||||
|
.attachment {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.attachment:last-of-type {
|
||||||
|
margin-bottom: -1rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* END of talk bubbles */
|
/* END of talk bubbles */
|
||||||
/* END of topic/mesages view */
|
/* END of topic/mesages view */
|
||||||
|
|
||||||
@@ -1053,12 +1231,13 @@ table.permission-editor td.checkbox {
|
|||||||
.dialog-overlay {
|
.dialog-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
background-color: rgba(0,0,0,0.25);
|
background-color: rgba(255,255,255,0.90);
|
||||||
}
|
}
|
||||||
.modal-dialog {
|
.modal-dialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1340,10 +1519,14 @@ ul.tabbar li.active > a:after {
|
|||||||
.material-icons {
|
.material-icons {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.material-icons.small {
|
.material-icons.small {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
.material-icons.big {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
#self-avatar.material-icons, .avatar-box .material-icons {
|
#self-avatar.material-icons, .avatar-box .material-icons {
|
||||||
font-size: 2.4rem;
|
font-size: 2.4rem;
|
||||||
@@ -1470,15 +1653,9 @@ ul.tabbar li.active > a:after {
|
|||||||
border-top: 0.35rem solid #1e88e5;
|
border-top: 0.35rem solid #1e88e5;
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
-webkit-animation: spin 1.5s linear infinite;
|
|
||||||
animation: spin 1.5s linear infinite;
|
animation: spin 1.5s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@-webkit-keyframes spin {
|
|
||||||
0% { -webkit-transform: rotate(0deg); }
|
|
||||||
100% { -webkit-transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
0% { transform: rotate(0deg); }
|
||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ Sample JSON representation of the text above:
|
|||||||
{name: "MN", dataName: "val",
|
{name: "MN", dataName: "val",
|
||||||
pack: function(val) { return {val: val.slice(1)}; },
|
pack: function(val) { return {val: val.slice(1)}; },
|
||||||
re: /\B@(\w\w+)/g},
|
re: /\B@(\w\w+)/g},
|
||||||
// Hashtags #hashtag
|
// Hashtags #hashtag, like metion 2 or more characters.
|
||||||
{name: "HT", dataName: "val",
|
{name: "HT", dataName: "val",
|
||||||
pack: function(val) { return {val: val.slice(1)}; },
|
pack: function(val) { return {val: val.slice(1)}; },
|
||||||
re: /\B#(\w\w+)/g}
|
re: /\B#(\w\w+)/g}
|
||||||
@@ -88,9 +88,23 @@ Sample JSON representation of the text above:
|
|||||||
BR: { name: 'br', isVoid: true },
|
BR: { name: 'br', isVoid: true },
|
||||||
LN: { name: 'a', isVoid: false },
|
LN: { name: 'a', isVoid: false },
|
||||||
MN: { name: 'a', isVoid: false },
|
MN: { name: 'a', isVoid: false },
|
||||||
HT: { name: 'a', isVoid: false }
|
HT: { name: 'a', isVoid: false },
|
||||||
|
IM: { name: 'img', isVoid: true }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Convert base64-encoded string into Blob.
|
||||||
|
function base64toObjectUrl(b64, contentType) {
|
||||||
|
var bin = atob(b64);
|
||||||
|
var length = bin.length;
|
||||||
|
var buf = new ArrayBuffer(length);
|
||||||
|
var arr = new Uint8Array(buf);
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
arr[i] = bin.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
return URL.createObjectURL(new Blob([buf], {type: contentType}));
|
||||||
|
}
|
||||||
|
|
||||||
// Helpers for converting Drafty to HTML.
|
// Helpers for converting Drafty to HTML.
|
||||||
var DECORATORS = {
|
var DECORATORS = {
|
||||||
ST: { open: function() { return '<b>'; }, close: function() { return '</b>'; }},
|
ST: { open: function() { return '<b>'; }, close: function() { return '</b>'; }},
|
||||||
@@ -112,6 +126,32 @@ Sample JSON representation of the text above:
|
|||||||
open: function(data) { return '<a href="#' + data.val + '">'; },
|
open: function(data) { return '<a href="#' + data.val + '">'; },
|
||||||
close: function(data) { return '</a>'; },
|
close: function(data) { return '</a>'; },
|
||||||
props: function(data) { return { name: data.val }; },
|
props: function(data) { return { name: data.val }; },
|
||||||
|
},
|
||||||
|
IM: {
|
||||||
|
open: function(data) {
|
||||||
|
var blobUrl = base64toObjectUrl(data.val, data.mime);
|
||||||
|
var res = (data.name ? '<a href="' + blobUrl + '" download="' + data.name + '">' : '') +
|
||||||
|
'<img src="' + blobUrl + '"' +
|
||||||
|
(data.width ? ' width="' + data.width + '"' : '') +
|
||||||
|
(data.height ? ' height="' + data.height + '"' : '') + ' border="0" />';
|
||||||
|
console.log("open: " + res);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
close: function(data) {
|
||||||
|
return (data.name ? '</a>' : '');
|
||||||
|
},
|
||||||
|
props: function(data) {
|
||||||
|
var url = base64toObjectUrl(data.val, data.mime);
|
||||||
|
return {
|
||||||
|
src: url,
|
||||||
|
title: data.name,
|
||||||
|
'data-width': data.width,
|
||||||
|
'data-height': data.height,
|
||||||
|
'data-name': data.name,
|
||||||
|
'data-size': (data.val.length * 0.75) | 0,
|
||||||
|
'data-mime': data.mime
|
||||||
|
};
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -340,10 +380,16 @@ Sample JSON representation of the text above:
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse text into structured representation.
|
* Parse plain text into structured representation.
|
||||||
*/
|
*/
|
||||||
parse: function(content) {
|
parse: function(content) {
|
||||||
|
// Make sure we are parsing strings only.
|
||||||
|
if (typeof content != "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Split text into lines. It makes further processing easier.
|
// Split text into lines. It makes further processing easier.
|
||||||
var lines = content.split(/\r?\n/);
|
var lines = content.split(/\r?\n/);
|
||||||
|
|
||||||
@@ -443,6 +489,64 @@ Sample JSON representation of the text above:
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add inline image to drafty content
|
||||||
|
*
|
||||||
|
* @param {Drafty} content object to add image to
|
||||||
|
* @param {integer} at index where the object is inserted
|
||||||
|
* @param {string} mime mime-type of the image, e.g. "image/png"
|
||||||
|
* @param {string} bits base64-encoded image content
|
||||||
|
* @param {integer} width width of the image
|
||||||
|
* @param {integer} height height of the image
|
||||||
|
* @param {string} fname file name suggestion for downloading the image.
|
||||||
|
*/
|
||||||
|
insertImage: function(content, at, mime, base64bits, width, height, fname) {
|
||||||
|
content = content || {txt: " "};
|
||||||
|
content.ent = content.ent || [];
|
||||||
|
content.fmt = content.fmt || [];
|
||||||
|
|
||||||
|
content.fmt.push({
|
||||||
|
at: at,
|
||||||
|
len: 1,
|
||||||
|
key: content.ent.length
|
||||||
|
});
|
||||||
|
content.ent.push({
|
||||||
|
tp: "IM",
|
||||||
|
data: {
|
||||||
|
mime: mime,
|
||||||
|
val: base64bits,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
name: fname
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return content;
|
||||||
|
},
|
||||||
|
|
||||||
|
attachFile: function(content, mime, base64bits, fname) {
|
||||||
|
content = content || {txt: ""};
|
||||||
|
content.ent = content.ent || [];
|
||||||
|
content.fmt = content.fmt || [];
|
||||||
|
|
||||||
|
content.fmt.push({
|
||||||
|
at: -1,
|
||||||
|
len: 0,
|
||||||
|
key: content.ent.length
|
||||||
|
});
|
||||||
|
|
||||||
|
content.ent.push({
|
||||||
|
tp: "EX",
|
||||||
|
data: {
|
||||||
|
mime: mime,
|
||||||
|
val: base64bits,
|
||||||
|
name: fname
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return content;
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given the structured representation of rich text, convert it to HTML.
|
* Given the structured representation of rich text, convert it to HTML.
|
||||||
* No attempt is made to strip pre-existing html markup.
|
* No attempt is made to strip pre-existing html markup.
|
||||||
@@ -470,8 +574,10 @@ Sample JSON representation of the text above:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (DECORATORS[tp]) {
|
if (DECORATORS[tp]) {
|
||||||
markup.push({idx: range.at, what: DECORATORS[tp].open(data)});
|
// Because we later sort in descending order, closing markup must come first.
|
||||||
|
// Otherwise zero-length objects will not be represented correctly.
|
||||||
markup.push({idx: range.at + range.len, what: DECORATORS[tp].close(data)});
|
markup.push({idx: range.at + range.len, what: DECORATORS[tp].close(data)});
|
||||||
|
markup.push({idx: range.at, what: DECORATORS[tp].open(data)});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -501,12 +607,20 @@ Sample JSON representation of the text above:
|
|||||||
format: function(content, formatter, context) {
|
format: function(content, formatter, context) {
|
||||||
var {txt, fmt, ent} = content;
|
var {txt, fmt, ent} = content;
|
||||||
|
|
||||||
|
txt = txt || "";
|
||||||
|
|
||||||
if (!fmt) {
|
if (!fmt) {
|
||||||
return [txt];
|
return [txt];
|
||||||
}
|
}
|
||||||
|
|
||||||
var spans = [].concat(fmt);
|
var spans = [].concat(fmt);
|
||||||
|
|
||||||
|
// Zero values may have been stripped. Restore them.
|
||||||
|
spans.map(function(s) {
|
||||||
|
s.at = s.at || 0;
|
||||||
|
s.len = s.len || 0;
|
||||||
|
});
|
||||||
|
|
||||||
// Soft spans first by start index (asc) then by length (desc).
|
// Soft spans first by start index (asc) then by length (desc).
|
||||||
spans.sort(function(a, b) {
|
spans.sort(function(a, b) {
|
||||||
if (a.at - b.at == 0) {
|
if (a.at - b.at == 0) {
|
||||||
@@ -520,7 +634,8 @@ Sample JSON representation of the text above:
|
|||||||
spans = spans.map(function(s) {
|
spans = spans.map(function(s) {
|
||||||
var data;
|
var data;
|
||||||
var tp = s.tp;
|
var tp = s.tp;
|
||||||
if (s.key + 1 > 0) {
|
if (!tp) {
|
||||||
|
s.key = s.key || 0;
|
||||||
data = ent[s.key].data;
|
data = ent[s.key].data;
|
||||||
tp = ent[s.key].tp;
|
tp = ent[s.key].tp;
|
||||||
}
|
}
|
||||||
@@ -542,13 +657,60 @@ Sample JSON representation of the text above:
|
|||||||
/**
|
/**
|
||||||
* Returns true if content has no markup and no entities.
|
* Returns true if content has no markup and no entities.
|
||||||
*
|
*
|
||||||
* @param {drafty} content - content to check for presence of markup.
|
* @param {Drafty} content - content to check for presence of markup.
|
||||||
* @returns true is content is plain text, false otherwise.
|
* @returns true is content is plain text, false otherwise.
|
||||||
*/
|
*/
|
||||||
isPlainText: function(content, noLineBreaks) {
|
isPlainText: function(content, noLineBreaks) {
|
||||||
return !(content.fmt || content.ent);
|
return !(content.fmt || content.ent);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the drafty content has attachments.
|
||||||
|
*
|
||||||
|
* @param {Drafty} content - content to check for attachments.
|
||||||
|
* @returns true if there are attachments.
|
||||||
|
*/
|
||||||
|
hasAttachment: function(content) {
|
||||||
|
if (content.ent && content.ent.length > 0) {
|
||||||
|
for (var i in content.ent) {
|
||||||
|
if (content.ent[i].tp == "EX") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumerate attachments
|
||||||
|
*
|
||||||
|
* @param {Drafty} content - drafty object to process for attachments.
|
||||||
|
* @param {function} callback - callback to call for each attachment.
|
||||||
|
* @param {Object} content - value of "this" for callback.
|
||||||
|
*/
|
||||||
|
attachments: function(content, callback, context) {
|
||||||
|
if (content.ent && content.ent.length > 0) {
|
||||||
|
for (var i in content.ent) {
|
||||||
|
if (content.ent[i].tp == "EX") {
|
||||||
|
callback.call(context, content.ent[i].data, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Get blob URL for a given entity
|
||||||
|
getBlobUrl: function(entity) {
|
||||||
|
return base64toObjectUrl(entity.val, entity.mime);
|
||||||
|
},
|
||||||
|
|
||||||
|
getAttachmentSize: function(entity) {
|
||||||
|
// Value is base64 encoded. The actual object size is smaller than the encoded length.
|
||||||
|
return entity.val ? (entity.val.length * 0.75) | 0 : 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
getAttachmentType: function(entity) {
|
||||||
|
return entity.mime || "text/plain";
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* Get HTML tag for a given two-letter style name
|
* Get HTML tag for a given two-letter style name
|
||||||
* @param {string} style - two-letter style, like ST or LN
|
* @param {string} style - two-letter style, like ST or LN
|
||||||
@@ -562,7 +724,7 @@ Sample JSON representation of the text above:
|
|||||||
* For a given data bundle generate an object with HTML attributes,
|
* For a given data bundle generate an object with HTML attributes,
|
||||||
* for instance, given {url: "http://www.example.com/"} return
|
* for instance, given {url: "http://www.example.com/"} return
|
||||||
* {href: "http://www.example.com/"}
|
* {href: "http://www.example.com/"}
|
||||||
* @param {stting} style - tw-letter style to generate attributes for.
|
* @param {string} style - tw-letter style to generate attributes for.
|
||||||
* @param {Object} data - data bundle to convert to attributes
|
* @param {Object} data - data bundle to convert to attributes
|
||||||
* @returns object with HTML attributes.
|
* @returns object with HTML attributes.
|
||||||
*/
|
*/
|
||||||
@@ -581,7 +743,6 @@ Sample JSON representation of the text above:
|
|||||||
getContentType: function() {
|
getContentType: function() {
|
||||||
return "text/x-drafty";
|
return "text/x-drafty";
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
+801
-269
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 748 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Reference in New Issue
Block a user