Feature🚀: Ability to mention anyone in open chats and notify them (#5460) [deploy]
* Feature 🚀 : Ability to delete messages in chat channels - Sending message ID to frontend - Deleting Message - Use pusher to delete message realtime * Minor Bug 🐞: Show message action only for current user - User can delete or edit their own messages * Test cases added * Bug 🐞: Update message id for receiver Message id was not sent to receiver by pusher * Refactoring🛠: Message controller refactoring * Test Cases📝 : Specs for Delete message added * Feature 🚀 : Ability to edit messages * Test Cases📝 : Specs for Edit message added * Feature 🚀 : Ability to mention member in Group [Frontend] * Test Cases📝 : Test Cases added * Feature 🚀 : Ability to mention member in Group [Backend] * Bug Fix 🐞: Making messages realtime for open channels * Feature 🚀 and Bug Fix 🐞: Ability to get notification if mentioned in open messages and increasing message number count * Feature 🚀 : Ability to remove message count on channel open * Bug Fix 🐞: Making Notification more reliable by checking if the new message is notified to user or not * Feature 🚀 : Ability to metion @all and notify them * Bug Fix 🐞: Minor fixes * Fews tweaks in code * Bug Fix 🐞: Feature was not present for Invite only Section * Bug Fix 🐞: In open channels one can't mention all * Bug Fix 🐞: Markdown conversion for all only in case of invite only * Specs added * Delete schema.rb * Schema file updated * Code refactoring * Further Code refactoring backend * Further Code refactoring backend * Bug Fix 🐞: Mention List error problem in switching tabs * Test Snapshots added * Feature 🚀 : Ability to mention member in Group [Frontend] * Test Cases📝 : Test Cases added * Feature 🚀 : Ability to mention member in Group [Backend] * Bug Fix 🐞: Making messages realtime for open channels * Feature 🚀 and Bug Fix 🐞: Ability to get notification if mentioned in open messages and increasing message number count * Feature 🚀 : Ability to remove message count on channel open * Bug Fix 🐞: Making Notification more reliable by checking if the new message is notified to user or not * Feature 🚀 : Ability to metion @all and notify them * Bug Fix 🐞: Minor fixes * Fews tweaks in code * Bug Fix 🐞: Feature was not present for Invite only Section * Bug Fix 🐞: In open channels one can't mention all * Bug Fix 🐞: Markdown conversion for all only in case of invite only * Specs added * Code refactoring * Further Code refactoring backend * Further Code refactoring backend * Bug Fix 🐞: Mention List error problem in switching tabs * Test Snapshots added * Feature 🚀: Ability to navigate through user list with keyboard only * Bug Fix 🐞: Changing color of group avatars in dark themes * Bug Fix 🐞: Setup pusher channel for open channels also
This commit is contained in:
parent
25e985cfce
commit
75608e3e55
19 changed files with 748 additions and 316 deletions
|
|
@ -63,9 +63,11 @@
|
|||
margin-bottom: 5px;
|
||||
|
||||
&:hover {
|
||||
@include themeable(background,
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-background-hover,
|
||||
lighten($green, 20%));
|
||||
lighten($green, 20%)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -138,12 +140,8 @@
|
|||
padding: 8px;
|
||||
font-size: 14px;
|
||||
-webkit-appearance: none;
|
||||
@include themeable(background,
|
||||
theme-container-background,
|
||||
white);
|
||||
@include themeable(color,
|
||||
theme-color,
|
||||
$black);
|
||||
@include themeable(background, theme-container-background, white);
|
||||
@include themeable(color, theme-color, $black);
|
||||
|
||||
@media screen and (min-width: 1000px) {
|
||||
width: calc(100% - 13px);
|
||||
|
|
@ -175,9 +173,7 @@
|
|||
}
|
||||
|
||||
.chat__channeltypefilterbutton--active {
|
||||
@include themeable(color,
|
||||
theme-anchor-color,
|
||||
darken($sky-blue, 5%));
|
||||
@include themeable(color, theme-anchor-color, darken($sky-blue, 5%));
|
||||
|
||||
@media screen and (min-width: 1300px) {
|
||||
border: 1px solid darken($light-medium-gray, 5%);
|
||||
|
|
@ -185,10 +181,7 @@
|
|||
}
|
||||
|
||||
.chat__channeltypefilterbutton--inactive {
|
||||
@include themeable(color,
|
||||
theme-secondary-color,
|
||||
lighten($medium-gray, 5%),
|
||||
);
|
||||
@include themeable(color, theme-secondary-color, lighten($medium-gray, 5%));
|
||||
}
|
||||
|
||||
.chat_chatconfig {
|
||||
|
|
@ -200,9 +193,7 @@
|
|||
padding: 3px 12px;
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
@include themeable(background,
|
||||
theme-background,
|
||||
$lightest-gray);
|
||||
@include themeable(background, theme-background, $lightest-gray);
|
||||
border: 1px solid $light-medium-gray;
|
||||
font-weight: bold;
|
||||
cursor: default;
|
||||
|
|
@ -305,7 +296,7 @@
|
|||
box-shadow: 2px 2px 2px 1px rgba(0, 0, 0, 0.4);
|
||||
|
||||
video {
|
||||
border-radius: 5px
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -318,9 +309,7 @@
|
|||
top: 0;
|
||||
padding: 80px 7%;
|
||||
z-index: 10;
|
||||
@include themeable(background,
|
||||
theme-background,
|
||||
white);
|
||||
@include themeable(background, theme-background, white);
|
||||
}
|
||||
|
||||
.chatNonChatView_exitbutton {
|
||||
|
|
@ -331,9 +320,7 @@
|
|||
background: transparent;
|
||||
border: 0px;
|
||||
font-size: 1.8em;
|
||||
@include themeable(color,
|
||||
theme-color,
|
||||
$black);
|
||||
@include themeable(color, theme-color, $black);
|
||||
}
|
||||
|
||||
.chatNonChatView_contentblock {
|
||||
|
|
@ -341,9 +328,7 @@
|
|||
padding: 20px;
|
||||
margin-bottom: 10px;
|
||||
max-width: 900px;
|
||||
@include themeable(box-shadow,
|
||||
theme-container-box-shadow,
|
||||
$bold-shadow);
|
||||
@include themeable(box-shadow, theme-container-box-shadow, $bold-shadow);
|
||||
|
||||
h2 {
|
||||
margin: 5px auto 5px;
|
||||
|
|
@ -402,22 +387,19 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
@include themeable(border,
|
||||
theme-container-border,
|
||||
1px solid $outline-color);
|
||||
@include themeable(box-shadow,
|
||||
theme-container-box-shadow,
|
||||
$bold-shadow);
|
||||
@include themeable(background,
|
||||
theme-container-background,
|
||||
white);
|
||||
@include themeable(border, theme-container-border, 1px solid $outline-color);
|
||||
@include themeable(box-shadow, theme-container-box-shadow, $bold-shadow);
|
||||
@include themeable(background, theme-container-background, white);
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
.activechatchannel__conversation {
|
||||
@include themeable(border-top,
|
||||
position: relative;
|
||||
@include themeable(
|
||||
border-top,
|
||||
theme-container-border,
|
||||
1px solid $outline-color);
|
||||
1px solid $outline-color
|
||||
);
|
||||
order: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -433,21 +415,23 @@
|
|||
}
|
||||
|
||||
.activechatchannel__header {
|
||||
@include themeable(border-bottom,
|
||||
@include themeable(
|
||||
border-bottom,
|
||||
theme-container-border,
|
||||
1px solid $light-medium-gray);
|
||||
1px solid $light-medium-gray
|
||||
);
|
||||
padding: 10px 12px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
@include themeable(background,
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-accent-background,
|
||||
$light-gray);
|
||||
$light-gray
|
||||
);
|
||||
position: relative;
|
||||
|
||||
a {
|
||||
@include themeable(color,
|
||||
theme-color,
|
||||
$black);
|
||||
@include themeable(color, theme-color, $black);
|
||||
}
|
||||
|
||||
.activechatchannel__channelconfig {
|
||||
|
|
@ -456,9 +440,7 @@
|
|||
right: 15px;
|
||||
top: 10px;
|
||||
cursor: pointer;
|
||||
@include themeable(filter,
|
||||
theme-social-icon-invert,
|
||||
invert(0));
|
||||
@include themeable(filter, theme-social-icon-invert, invert(0));
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
|
|
@ -500,14 +482,15 @@
|
|||
}
|
||||
|
||||
.activechatchannel__form {
|
||||
@include themeable(border-top,
|
||||
@include themeable(
|
||||
border-top,
|
||||
theme-container-border,
|
||||
1px solid $outline-color);
|
||||
@include themeable(background,
|
||||
theme-container-accent-background,
|
||||
#ededed);
|
||||
1px solid $outline-color
|
||||
);
|
||||
@include themeable(background, theme-container-accent-background, #ededed);
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.activechatchannel__incomingcall {
|
||||
|
|
@ -525,18 +508,14 @@
|
|||
.activechatchannel__activecontent {
|
||||
order: 2;
|
||||
flex: 0 1 auto;
|
||||
@include themeable(border,
|
||||
theme-border,
|
||||
1px solid $outline-color);
|
||||
@include themeable(border, theme-border, 1px solid $outline-color);
|
||||
padding: 13px;
|
||||
min-width: 50%;
|
||||
@media screen and (min-width: 1440px) {
|
||||
min-width: 45%;
|
||||
}
|
||||
-webkit-overflow-scrolling: touch;
|
||||
@include themeable(background,
|
||||
theme-background,
|
||||
$lightest-gray);
|
||||
@include themeable(background, theme-background, $lightest-gray);
|
||||
z-index: 200;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
|
@ -576,27 +555,20 @@
|
|||
.activechatchannel__activecontentexitbutton {
|
||||
border: 0px;
|
||||
font-size: 30px;
|
||||
@include themeable(color,
|
||||
theme-container-color,
|
||||
$medium-gray);
|
||||
@include themeable(color, theme-container-color, $medium-gray);
|
||||
padding: 0px;
|
||||
background: transparent;
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
top: 4px;
|
||||
@include themeable(background,
|
||||
theme-container-accent-background,
|
||||
white);
|
||||
@include themeable(background, theme-container-accent-background, white);
|
||||
z-index: 10;
|
||||
height: 29px;
|
||||
width: 29px;
|
||||
line-height: 10px;
|
||||
padding-bottom: 5px;
|
||||
border-radius: 100px;
|
||||
@include themeable(border,
|
||||
theme-border,
|
||||
1px solid $medium-gray);
|
||||
|
||||
@include themeable(border, theme-border, 1px solid $medium-gray);
|
||||
}
|
||||
|
||||
.activechatchannel__activeArticle {
|
||||
|
|
@ -618,7 +590,6 @@
|
|||
border: 0px;
|
||||
}
|
||||
|
||||
|
||||
.activechatchannel__activecontentuserdetails {
|
||||
margin-top: 20px;
|
||||
font-family: $monospace;
|
||||
|
|
@ -627,7 +598,7 @@
|
|||
.key {
|
||||
color: $medium-gray;
|
||||
margin-bottom: 3px;
|
||||
font-size: 0.9em
|
||||
font-size: 0.9em;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -674,7 +645,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.activecontent__githubrepo {}
|
||||
.activecontent__githubrepo {
|
||||
}
|
||||
|
||||
.activecontent__githubrepoheader {
|
||||
padding-left: 30px;
|
||||
|
|
@ -738,9 +710,7 @@
|
|||
padding: 3px 0px;
|
||||
margin-bottom: -5px 0;
|
||||
box-sizing: border-box;
|
||||
@include themeable(color,
|
||||
theme-color,
|
||||
$black);
|
||||
@include themeable(color, theme-color, $black);
|
||||
|
||||
&:hover {
|
||||
.chatchanneltab--inactive {
|
||||
|
|
@ -773,9 +743,7 @@
|
|||
}
|
||||
|
||||
.chatchanneltab--active {
|
||||
@include themeable(background,
|
||||
theme-container-background,
|
||||
white);
|
||||
@include themeable(background, theme-container-background, white);
|
||||
border: 1px solid $dark-purple;
|
||||
box-shadow: 3px 3px 0px $dark-purple;
|
||||
}
|
||||
|
|
@ -807,14 +775,12 @@
|
|||
border: 2px solid $light-medium-gray;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.chatchanneltabgroupicon {
|
||||
display: inline-block;
|
||||
margin-left: calc(20px + 0.3vw);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.chatchanneltabindicator--new {
|
||||
|
|
@ -863,9 +829,7 @@
|
|||
bottom: -14px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
@include themeable(background,
|
||||
theme-background,
|
||||
$lightest-gray);
|
||||
@include themeable(background, theme-background, $lightest-gray);
|
||||
border-top: 1px solid $light-medium-gray;
|
||||
padding: 8px 10px 20px;
|
||||
font-weight: bold;
|
||||
|
|
@ -904,7 +868,7 @@
|
|||
|
||||
.chatchannels__misc {
|
||||
height: 70px;
|
||||
border-top: 1px solid #c9c9c9
|
||||
border-top: 1px solid #c9c9c9;
|
||||
}
|
||||
|
||||
// Chatmessage
|
||||
|
|
@ -914,24 +878,30 @@
|
|||
max-width: 100%;
|
||||
|
||||
&:hover {
|
||||
@include themeable(background,
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-background-hover,
|
||||
$light-gray);
|
||||
$light-gray
|
||||
);
|
||||
|
||||
.chatmessagebody__username--link {
|
||||
@include themeable(background,
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-background-hover,
|
||||
transparent);
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.chatmessage__profileimage {}
|
||||
.invert-channel-image {
|
||||
filter: var(--theme-invert-image, none);
|
||||
}
|
||||
.chatmessage__profileimage {
|
||||
}
|
||||
|
||||
.chatmessage__body {
|
||||
flex-grow: 1;
|
||||
max-width: calc(100% - 37px)
|
||||
max-width: calc(100% - 37px);
|
||||
}
|
||||
|
||||
.chatmessagebody__username {
|
||||
|
|
@ -990,13 +960,16 @@
|
|||
display: inline-block;
|
||||
|
||||
&:hover {
|
||||
@include themeable(background,
|
||||
@include themeable(
|
||||
background,
|
||||
theme-container-background-hover,
|
||||
transparent);
|
||||
transparent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
.chatmessagebody__divider {}
|
||||
.chatmessagebody__divider {
|
||||
}
|
||||
|
||||
.chatmessagebody__message {
|
||||
p {
|
||||
|
|
@ -1015,12 +988,8 @@
|
|||
}
|
||||
|
||||
a h1 {
|
||||
@include themeable(color,
|
||||
theme-container-color,
|
||||
$black);
|
||||
@include themeable-important(border-color,
|
||||
theme-container-color,
|
||||
#0a0a0a);
|
||||
@include themeable(color, theme-container-color, $black);
|
||||
@include themeable-important(border-color, theme-container-color, #0a0a0a);
|
||||
}
|
||||
|
||||
img {
|
||||
|
|
@ -1050,15 +1019,9 @@
|
|||
padding: 5px;
|
||||
resize: none;
|
||||
flex-grow: 1;
|
||||
@include themeable(background,
|
||||
theme-container-background,
|
||||
white);
|
||||
@include themeable(border,
|
||||
theme-border,
|
||||
1px solid $outline-color);
|
||||
@include themeable(color,
|
||||
theme-color,
|
||||
$black);
|
||||
@include themeable(background, theme-container-background, white);
|
||||
@include themeable(border, theme-border, 1px solid $outline-color);
|
||||
@include themeable(color, theme-color, $black);
|
||||
}
|
||||
|
||||
.messagecomposer__submit {
|
||||
|
|
@ -1096,9 +1059,7 @@
|
|||
top: -27px;
|
||||
left: 35px;
|
||||
font-family: $monospace;
|
||||
@include themeable(color,
|
||||
theme-color,
|
||||
$dark-gray);
|
||||
@include themeable(color, theme-color, $dark-gray);
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
|
|
@ -1166,7 +1127,6 @@
|
|||
display: none;
|
||||
justify-content: end;
|
||||
-webkit-justify-content: flex-end;
|
||||
|
||||
}
|
||||
|
||||
.message__actions span {
|
||||
|
|
@ -1190,9 +1150,7 @@
|
|||
max-width: 670px;
|
||||
padding: 0px;
|
||||
margin-bottom: 6px;
|
||||
@include themeable(background,
|
||||
theme-container-background,
|
||||
white);
|
||||
@include themeable(background, theme-container-background, white);
|
||||
.chatchannels__richlinkmainimage {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
|
@ -1212,18 +1170,14 @@
|
|||
vertical-align: -0.25em;
|
||||
&.chatchannels__richlinkprofilepic {
|
||||
border-radius: 100px;
|
||||
@include themeable(border,
|
||||
theme-border,
|
||||
1px solid $light-medium-gray);
|
||||
@include themeable(border, theme-border, 1px solid $light-medium-gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
h4 {
|
||||
margin-top: -12px;
|
||||
margin-bottom: 15px;
|
||||
@include themeable(color,
|
||||
theme-secondary-color,
|
||||
$medium-gray);
|
||||
@include themeable(color, theme-secondary-color, $medium-gray);
|
||||
font-weight: 500;
|
||||
padding: 0px calc(10px + 0.2vw);
|
||||
img {
|
||||
|
|
@ -1231,9 +1185,7 @@
|
|||
width: 22px;
|
||||
border-radius: 100px;
|
||||
margin-right: 3px;
|
||||
@include themeable(border,
|
||||
theme-border,
|
||||
1px solid $light-medium-gray);
|
||||
@include themeable(border, theme-border, 1px solid $light-medium-gray);
|
||||
vertical-align: -6px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1320,3 +1272,50 @@
|
|||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mention__list {
|
||||
position: absolute;
|
||||
background: var(--theme-container-background, white);
|
||||
display: none;
|
||||
z-index: 1;
|
||||
border-top: 1px solid #dbdbdb;
|
||||
bottom: 60px;
|
||||
width: 100%;
|
||||
max-height: 280px;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.mention__list .mention__user {
|
||||
padding: 5px 10px;
|
||||
border-bottom: 1px solid #dbdbdb;
|
||||
cursor: pointer;
|
||||
}
|
||||
.compose__outer__container {
|
||||
position: relative;
|
||||
}
|
||||
.mention__user__image {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
margin-right: 6px;
|
||||
border-radius: 20px;
|
||||
align-self: center;
|
||||
}
|
||||
.mention__visible {
|
||||
display: block;
|
||||
}
|
||||
.mention__user {
|
||||
display: grid;
|
||||
grid-template-columns: 30px 1fr;
|
||||
}
|
||||
.mention__user p {
|
||||
margin: 0px;
|
||||
font-size: 9px;
|
||||
color: gray;
|
||||
width: max-content;
|
||||
}
|
||||
.comment-mentioned-all {
|
||||
color: rgb(213, 138, 0);
|
||||
}
|
||||
|
||||
.active__message__list {
|
||||
background: var(--theme-container-accent-background, #f5f6f7);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.rich-selector__radio:focus~.rich-selector__outline {
|
||||
.rich-selector__radio:focus ~ .rich-selector__outline {
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
right: 7px;
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
border: 2px solid darken($green, 10%);
|
||||
}
|
||||
|
||||
.rich-selector__radio:checked+.rich-selector__indicator {
|
||||
.rich-selector__radio:checked + .rich-selector__indicator {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
|
|
@ -133,10 +133,10 @@
|
|||
}
|
||||
|
||||
.theme-selector--pink-theme {
|
||||
--theme-background: #FFF7F9;
|
||||
--theme-background: #fff7f9;
|
||||
--theme-color: #0a0a0a;
|
||||
--theme-top-bar-background: #ff4983;
|
||||
--theme-top-bar-search-background: #FFF7F9;
|
||||
--theme-top-bar-search-background: #fff7f9;
|
||||
--theme-container-background: #fff;
|
||||
--theme-container-border: 1px solid #ff4983;
|
||||
}
|
||||
|
|
@ -170,13 +170,8 @@
|
|||
}
|
||||
|
||||
.font-selector--default {
|
||||
font-family:
|
||||
Palatino,
|
||||
'Palatino Linotype',
|
||||
'Palatino LT STD',
|
||||
'Book Antiqua',
|
||||
Georgia,
|
||||
serif;
|
||||
font-family: Palatino, 'Palatino Linotype', 'Palatino LT STD', 'Book Antiqua',
|
||||
Georgia, serif;
|
||||
}
|
||||
|
||||
.font-selector--sans-serif {
|
||||
|
|
@ -198,7 +193,6 @@
|
|||
font-size: 1.33em;
|
||||
}
|
||||
|
||||
|
||||
// Navbar selector
|
||||
|
||||
.navbar-selector {
|
||||
|
|
@ -234,7 +228,7 @@
|
|||
border: 4px solid $green !important;
|
||||
}
|
||||
|
||||
.rich-selector__radio:focus~.exp-selector__outline {
|
||||
.rich-selector__radio:focus ~ .exp-selector__outline {
|
||||
border-radius: 100px;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ class ChatChannelsController < ApplicationController
|
|||
def open
|
||||
membership = @chat_channel.chat_channel_memberships.where(user_id: current_user.id).first
|
||||
membership.update(last_opened_at: 1.second.from_now, has_unopened_messages: false)
|
||||
send_open_notification
|
||||
@chat_channel.index!
|
||||
membership.index!
|
||||
render json: { status: "success", channel: params[:id] }, status: :ok
|
||||
|
|
@ -181,4 +182,13 @@ class ChatChannelsController < ApplicationController
|
|||
render json: { errors: @chat_channel.errors.full_messages }
|
||||
end
|
||||
end
|
||||
|
||||
def send_open_notification
|
||||
adjusted_slug = if @chat_channel.group?
|
||||
@chat_channel.adjusted_slug
|
||||
else
|
||||
@chat_channel.adjusted_slug(current_user)
|
||||
end
|
||||
Pusher.trigger("private-message-notifications-#{session_current_user_id}", "message-opened", { channel_type: @chat_channel.channel_type, adjusted_slug: adjusted_slug }.to_json)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ class MessagesController < ApplicationController
|
|||
pusher_message_created(true)
|
||||
if @message.save
|
||||
pusher_message_created(false)
|
||||
notify_users(@message.chat_channel.channel_users_ids, "all")
|
||||
notify_users(@mentioned_users_id, "mention")
|
||||
render json: { status: "success", message: { temp_id: @temp_message_id, id: @message.id } }, status: :created
|
||||
else
|
||||
render json: {
|
||||
|
|
@ -78,12 +80,13 @@ class MessagesController < ApplicationController
|
|||
private
|
||||
|
||||
def create_pusher_payload(new_message, temp_id)
|
||||
{
|
||||
payload = {
|
||||
temp_id: temp_id,
|
||||
id: new_message.id,
|
||||
user_id: new_message.user.id,
|
||||
chat_channel_id: new_message.chat_channel.id,
|
||||
chat_channel_adjusted_slug: new_message.chat_channel.adjusted_slug(current_user, "sender"),
|
||||
channel_type: new_message.chat_channel.channel_type,
|
||||
username: new_message.user.username,
|
||||
profile_image_url: ProfileImage.new(new_message.user).get(90),
|
||||
message: new_message.message_html,
|
||||
|
|
@ -92,10 +95,16 @@ class MessagesController < ApplicationController
|
|||
timestamp: Time.current,
|
||||
color: new_message.preferred_user_color,
|
||||
reception_method: "pushed"
|
||||
}.to_json
|
||||
}
|
||||
|
||||
if new_message.chat_channel.group?
|
||||
payload[:chat_channel_adjusted_slug] = new_message.chat_channel.adjusted_slug
|
||||
end
|
||||
payload.to_json
|
||||
end
|
||||
|
||||
def message_params
|
||||
@mentioned_users_id = params[:message][:mentioned_users_id]
|
||||
params.require(:message).permit(:message_markdown, :user_id, :chat_channel_id)
|
||||
end
|
||||
|
||||
|
|
@ -132,4 +141,17 @@ class MessagesController < ApplicationController
|
|||
logger.info "PUSHER ERROR: #{e.message}"
|
||||
end
|
||||
end
|
||||
|
||||
def notify_users(user_ids, type)
|
||||
return unless user_ids
|
||||
|
||||
user_ids.each do |id|
|
||||
message_json = create_pusher_payload(@message, @temp_message_id)
|
||||
if type == "mention"
|
||||
Pusher.trigger("private-message-notifications-#{id}", "mentioned", message_json)
|
||||
else
|
||||
Pusher.trigger("private-message-notifications-#{id}", "message-created", message_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ exports[`<Channels /> expanded with chat channels should render and test snapsho
|
|||
>
|
||||
<img
|
||||
alt="pic"
|
||||
class="chatchanneltabindicatordirectimage"
|
||||
class="chatchanneltabindicatordirectimage invert-channel-image"
|
||||
/>
|
||||
</span>
|
||||
channel name 1
|
||||
|
|
@ -86,7 +86,7 @@ exports[`<Channels /> expanded with chat channels should render and test snapsho
|
|||
>
|
||||
<img
|
||||
alt="pic"
|
||||
class="chatchanneltabindicatordirectimage"
|
||||
class="chatchanneltabindicatordirectimage invert-channel-image"
|
||||
/>
|
||||
</span>
|
||||
group channel 3
|
||||
|
|
@ -207,7 +207,7 @@ exports[`<Channels /> not expanded with chat channels should render and test sna
|
|||
>
|
||||
<img
|
||||
alt="pic"
|
||||
class="chatchanneltabindicatordirectimage"
|
||||
class="chatchanneltabindicatordirectimage invert-channel-image"
|
||||
/>
|
||||
</span>
|
||||
channel name 1
|
||||
|
|
@ -263,7 +263,7 @@ exports[`<Channels /> not expanded with chat channels should render and test sna
|
|||
>
|
||||
<img
|
||||
alt="pic"
|
||||
class="chatchanneltabindicatordirectimage"
|
||||
class="chatchanneltabindicatordirectimage invert-channel-image"
|
||||
/>
|
||||
</span>
|
||||
group channel 3
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ exports[`<Chat /> should load chat 1`] = `
|
|||
>
|
||||
<img
|
||||
alt="pic"
|
||||
class="chatchanneltabindicatordirectimage"
|
||||
class="chatchanneltabindicatordirectimage invert-channel-image"
|
||||
/>
|
||||
</span>
|
||||
channel name 1
|
||||
|
|
@ -123,7 +123,7 @@ exports[`<Chat /> should load chat 1`] = `
|
|||
>
|
||||
<img
|
||||
alt="pic"
|
||||
class="chatchanneltabindicatordirectimage"
|
||||
class="chatchanneltabindicatordirectimage invert-channel-image"
|
||||
/>
|
||||
</span>
|
||||
group channel 3
|
||||
|
|
@ -240,11 +240,19 @@ exports[`<Chat /> should load chat 1`] = `
|
|||
>
|
||||
More new messages below
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="mention__list"
|
||||
id="mentionList"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
class="activechatchannel__form"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="compose__outer__container"
|
||||
>
|
||||
<div
|
||||
class="messagecomposer"
|
||||
>
|
||||
|
|
@ -253,6 +261,8 @@ exports[`<Chat /> should load chat 1`] = `
|
|||
id="messageform"
|
||||
maxLength="1000"
|
||||
onKeyDown={[Function]}
|
||||
onKeyPress={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
placeholder="Message goes here"
|
||||
/>
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<Compose /> behavior with message should render and test snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="compose__outer__container"
|
||||
>
|
||||
<div
|
||||
class="messagecomposer"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export function getAllMessages(channelId, messageOffset, successCb, failureCb) {
|
|||
.catch(failureCb);
|
||||
}
|
||||
|
||||
export function sendMessage(activeChannelId, message, successCb, failureCb) {
|
||||
export function sendMessage(messageObject, successCb, failureCb) {
|
||||
fetch('/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -19,9 +19,10 @@ export function sendMessage(activeChannelId, message, successCb, failureCb) {
|
|||
},
|
||||
body: JSON.stringify({
|
||||
message: {
|
||||
message_markdown: message,
|
||||
message_markdown: messageObject.message,
|
||||
user_id: window.currentUser.id,
|
||||
chat_channel_id: activeChannelId,
|
||||
chat_channel_id: messageObject.activeChannelId,
|
||||
mentioned_users_id: messageObject.mentionedUsersId,
|
||||
},
|
||||
}),
|
||||
credentials: 'same-origin',
|
||||
|
|
|
|||
|
|
@ -111,12 +111,12 @@ class ChannelDetails extends Component {
|
|||
className="channeldetails__userprofileimage"
|
||||
src={user.profile_image}
|
||||
alt={`${user.username} profile`}
|
||||
data-content='sidecar-user'
|
||||
data-content="sidecar-user"
|
||||
/>
|
||||
<a
|
||||
href={`/${user.username}`}
|
||||
style={{ color: user.darker_color, padding: '3px 0px' }}
|
||||
data-content='sidecar-user'
|
||||
data-content="sidecar-user"
|
||||
>
|
||||
{user.name}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ const Channels = ({
|
|||
}) => {
|
||||
const channels = chatChannels.map(channel => {
|
||||
const isActive = parseInt(activeChannelId, 10) === channel.chat_channel_id;
|
||||
const isUnopened = !isActive && unopenedChannelIds.includes(channel.chat_channel_id)
|
||||
const isUnopened =
|
||||
!isActive && unopenedChannelIds.includes(channel.chat_channel_id);
|
||||
let newMessagesIndicator = isUnopened ? 'new' : 'old';
|
||||
if (incomingVideoCallChannelIds.indexOf(channel.chat_channel_id) > -1) {
|
||||
newMessagesIndicator = 'video';
|
||||
|
|
@ -49,7 +50,11 @@ const Channels = ({
|
|||
<img
|
||||
src={channel.channel_image}
|
||||
alt="pic"
|
||||
className="chatchanneltabindicatordirectimage"
|
||||
className={
|
||||
channel.channel_type === 'direct'
|
||||
? 'chatchanneltabindicatordirectimage'
|
||||
: 'chatchanneltabindicatordirectimage invert-channel-image'
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
{channel.channel_name}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export default class Chat extends Component {
|
|||
super(props);
|
||||
const chatChannels = JSON.parse(props.chatChannels);
|
||||
const chatOptions = JSON.parse(props.chatOptions);
|
||||
|
||||
this.state = {
|
||||
messages: [],
|
||||
scrolled: false,
|
||||
|
|
@ -72,6 +73,10 @@ export default class Chat extends Component {
|
|||
currentMessageLocation: 0,
|
||||
startEditing: false,
|
||||
activeEditMessage: {},
|
||||
markdownEdited: false,
|
||||
channelUsers: [],
|
||||
showMemberlist: false,
|
||||
memberFilterQuery: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -85,18 +90,21 @@ export default class Chat extends Component {
|
|||
channelPaginationNum,
|
||||
currentUserId,
|
||||
} = this.state;
|
||||
|
||||
this.setupChannels(chatChannels);
|
||||
|
||||
const channelsForPusherSub = chatChannels.filter(
|
||||
this.channelTypeFilter('open'),
|
||||
this.channelTypeFilterFn('open'),
|
||||
);
|
||||
this.subscribeChannelsToPusher(
|
||||
channelsForPusherSub,
|
||||
channel => `open-channel-${channel.chat_channel_id}`,
|
||||
);
|
||||
|
||||
setupObserver(this.observerCallback);
|
||||
|
||||
|
||||
this.subscribePusher(`private-message-notifications-${currentUserId}`);
|
||||
|
||||
|
||||
if (activeChannelId) {
|
||||
sendOpen(activeChannelId, this.handleChannelOpenSuccess, null);
|
||||
}
|
||||
|
|
@ -113,7 +121,7 @@ export default class Chat extends Component {
|
|||
filters,
|
||||
this.loadChannels,
|
||||
);
|
||||
getUnopenedChannelIds(this.markUnopenedChannelIds)
|
||||
getUnopenedChannelIds(this.markUnopenedChannelIds);
|
||||
}
|
||||
if (!isMobileDevice) {
|
||||
document.getElementById('messageform').focus();
|
||||
|
|
@ -184,6 +192,8 @@ export default class Chat extends Component {
|
|||
liveCoding: this.liveCoding,
|
||||
videoCallInitiated: this.receiveVideoCall,
|
||||
videoCallEnded: this.receiveVideoCallHangup,
|
||||
mentioned: this.mentioned,
|
||||
messageOpened: this.messageOpened,
|
||||
});
|
||||
const subscriptions = subscribedPusherChannels;
|
||||
subscriptions.push(channelName);
|
||||
|
|
@ -191,10 +201,13 @@ export default class Chat extends Component {
|
|||
}
|
||||
};
|
||||
|
||||
mentioned = () => {};
|
||||
|
||||
messageOpened = () => {};
|
||||
|
||||
loadChannels = (channels, query) => {
|
||||
const { activeChannelId, activeChannel } = this.state;
|
||||
if (activeChannelId && query.length === 0) {
|
||||
this.setupChannel(activeChannelId);
|
||||
this.setState({
|
||||
chatChannels: channels,
|
||||
scrolled: false,
|
||||
|
|
@ -205,15 +218,19 @@ export default class Chat extends Component {
|
|||
activeChannel ||
|
||||
this.filterForActiveChannel(channels, activeChannelId),
|
||||
});
|
||||
} else if (activeChannelId) {
|
||||
this.setupChannel(activeChannelId);
|
||||
} else if (activeChannelId) {
|
||||
this.setState({
|
||||
scrolled: false,
|
||||
chatChannels: channels,
|
||||
channelsLoaded: true,
|
||||
channelPaginationNum: 0,
|
||||
filterQuery: query,
|
||||
activeChannel:
|
||||
activeChannel ||
|
||||
this.filterForActiveChannel(channels, activeChannelId),
|
||||
});
|
||||
this.setupChannel(activeChannelId);
|
||||
} else if (channels.length > 0) {
|
||||
this.setState({
|
||||
chatChannels: channels,
|
||||
|
|
@ -231,15 +248,19 @@ export default class Chat extends Component {
|
|||
this.setState({ channelsLoaded: true });
|
||||
}
|
||||
this.subscribeChannelsToPusher(
|
||||
channels.filter(this.channelTypeFilter('invite_only')),
|
||||
channels.filter(this.channelTypeFilterFn('open')),
|
||||
channel => `open-channel-${channel.chat_channel_id}`,
|
||||
);
|
||||
this.subscribeChannelsToPusher(
|
||||
channels.filter(this.channelTypeFilterFn('invite_only')),
|
||||
channel => `presence-channel-${channel.chat_channel_id}`,
|
||||
);
|
||||
document.getElementById('chatchannels__channelslist').scrollTop = 0;
|
||||
};
|
||||
|
||||
markUnopenedChannelIds = (ids) => {
|
||||
this.setState({unopenedChannelIds: ids})
|
||||
}
|
||||
markUnopenedChannelIds = ids => {
|
||||
this.setState({ unopenedChannelIds: ids });
|
||||
};
|
||||
|
||||
subscribeChannelsToPusher = (channels, channelNameFn) => {
|
||||
channels.forEach(channel => {
|
||||
|
|
@ -247,7 +268,7 @@ export default class Chat extends Component {
|
|||
});
|
||||
};
|
||||
|
||||
channelTypeFilter = type => channel => {
|
||||
channelTypeFilterFn = type => channel => {
|
||||
return channel.channel_type === type;
|
||||
};
|
||||
|
||||
|
|
@ -283,7 +304,12 @@ export default class Chat extends Component {
|
|||
};
|
||||
|
||||
setupChannel = channelId => {
|
||||
const { messages, messageOffset } = this.state;
|
||||
const {
|
||||
messages,
|
||||
messageOffset,
|
||||
activeChannel,
|
||||
activeChannelId,
|
||||
} = this.state;
|
||||
if (
|
||||
!messages[channelId] ||
|
||||
messages[channelId].length === 0 ||
|
||||
|
|
@ -291,9 +317,44 @@ export default class Chat extends Component {
|
|||
) {
|
||||
getAllMessages(channelId, messageOffset, this.receiveAllMessages);
|
||||
}
|
||||
if (activeChannel && activeChannel.channel_type !== 'direct') {
|
||||
getContent(
|
||||
`/api/chat_channels/${activeChannelId}`,
|
||||
this.setOpenChannelUsers,
|
||||
null,
|
||||
);
|
||||
if (activeChannel.channel_type === 'open')
|
||||
this.subscribePusher(`open-channel-${channelId}`);
|
||||
}
|
||||
this.subscribePusher(`presence-channel-${channelId}`);
|
||||
};
|
||||
|
||||
setOpenChannelUsers = res => {
|
||||
const { activeChannelId, activeChannel } = this.state;
|
||||
Object.filter = (obj, predicate) =>
|
||||
Object.fromEntries(Object.entries(obj).filter(predicate));
|
||||
const leftUser = Object.filter(
|
||||
res.channel_users,
|
||||
([username]) => username !== window.currentUser.username,
|
||||
);
|
||||
if (activeChannel.channel_type === 'open') {
|
||||
this.setState({
|
||||
channelUsers: {
|
||||
[activeChannelId]: leftUser,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
channelUsers: {
|
||||
[activeChannelId]: {
|
||||
all: { username: 'all', name: 'To notify everyone here' },
|
||||
...leftUser,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
observerCallback = entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
|
|
@ -333,22 +394,28 @@ export default class Chat extends Component {
|
|||
|
||||
updateMessage = message => {
|
||||
const { activeChannelId } = this.state;
|
||||
|
||||
this.setState(({ messages }) => {
|
||||
const newMessages = messages;
|
||||
const foundIndex = messages[activeChannelId].findIndex(
|
||||
oldMessage => oldMessage.id === message.id,
|
||||
);
|
||||
newMessages[activeChannelId][foundIndex] = message;
|
||||
return { messages: newMessages };
|
||||
});
|
||||
if (message.chat_channel_id === activeChannelId) {
|
||||
this.setState(({ messages }) => {
|
||||
const newMessages = messages;
|
||||
const foundIndex = messages[activeChannelId].findIndex(
|
||||
oldMessage => oldMessage.id === message.id,
|
||||
);
|
||||
newMessages[activeChannelId][foundIndex] = message;
|
||||
return { messages: newMessages };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
receiveNewMessage = message => {
|
||||
const { messages, activeChannelId, scrolled, chatChannels, unopenedChannelIds } = this.state;
|
||||
const {
|
||||
messages,
|
||||
activeChannelId,
|
||||
scrolled,
|
||||
chatChannels,
|
||||
unopenedChannelIds,
|
||||
} = this.state;
|
||||
const receivedChatChannelId = message.chat_channel_id;
|
||||
let newMessages = [];
|
||||
|
||||
if (
|
||||
message.temp_id &&
|
||||
messages[activeChannelId].findIndex(
|
||||
|
|
@ -386,13 +453,13 @@ export default class Chat extends Component {
|
|||
if (receivedChatChannelId === activeChannelId) {
|
||||
sendOpen(receivedChatChannelId, this.handleChannelOpenSuccess, null);
|
||||
} else {
|
||||
const newUnopenedChannels = unopenedChannelIds
|
||||
const newUnopenedChannels = unopenedChannelIds;
|
||||
if (!unopenedChannelIds.includes(receivedChatChannelId)) {
|
||||
newUnopenedChannels.push(receivedChatChannelId)
|
||||
}
|
||||
newUnopenedChannels.push(receivedChatChannelId);
|
||||
}
|
||||
this.setState({
|
||||
unopenedChannelIds: newUnopenedChannels
|
||||
})
|
||||
unopenedChannelIds: newUnopenedChannels,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState(prevState => ({
|
||||
|
|
@ -467,13 +534,18 @@ export default class Chat extends Component {
|
|||
};
|
||||
|
||||
handleKeyDown = e => {
|
||||
const { showMemberlist } = this.state;
|
||||
const enterPressed = e.keyCode === 13;
|
||||
const targetValue = e.target.value;
|
||||
const messageIsEmpty = targetValue.length === 0;
|
||||
const shiftPressed = e.shiftKey;
|
||||
|
||||
if (enterPressed) {
|
||||
if (messageIsEmpty) {
|
||||
if (showMemberlist) {
|
||||
e.preventDefault();
|
||||
const selectedUser = document.querySelector('.active__message__list');
|
||||
this.addUserName({ target: selectedUser });
|
||||
} else if (messageIsEmpty) {
|
||||
e.preventDefault();
|
||||
} else if (!messageIsEmpty && !shiftPressed) {
|
||||
e.preventDefault();
|
||||
|
|
@ -481,6 +553,11 @@ export default class Chat extends Component {
|
|||
e.target.value = '';
|
||||
}
|
||||
}
|
||||
if (e.target.value.includes('@')) {
|
||||
if (e.keyCode === 40 || e.keyCode === 38) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDownEdit = e => {
|
||||
|
|
@ -529,8 +606,8 @@ export default class Chat extends Component {
|
|||
});
|
||||
this.setActiveContent({
|
||||
path: '/new',
|
||||
type_of: 'article'
|
||||
})
|
||||
type_of: 'article',
|
||||
});
|
||||
} else if (message.startsWith('/github')) {
|
||||
const args = message.split('/github ')[1].trim();
|
||||
this.setActiveContentState(activeChannelId, { type_of: 'github', args });
|
||||
|
|
@ -542,12 +619,12 @@ export default class Chat extends Component {
|
|||
this.handleFailure,
|
||||
);
|
||||
} else {
|
||||
sendMessage(
|
||||
const messageObject = {
|
||||
activeChannelId,
|
||||
message,
|
||||
this.handleSuccess,
|
||||
this.handleFailure,
|
||||
);
|
||||
mentionedUsersId: this.getMentionedUsers(message),
|
||||
};
|
||||
sendMessage(messageObject, this.handleSuccess, this.handleFailure);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -595,7 +672,7 @@ export default class Chat extends Component {
|
|||
|
||||
triggerSwitchChannel = (id, slug) => {
|
||||
const { chatChannels, isMobileDevice, unopenedChannelIds } = this.state;
|
||||
const newUnopenedChannelIds = unopenedChannelIds
|
||||
const newUnopenedChannelIds = unopenedChannelIds;
|
||||
const index = newUnopenedChannelIds.indexOf(id);
|
||||
if (index > -1) {
|
||||
newUnopenedChannelIds.splice(index, 1);
|
||||
|
|
@ -605,7 +682,9 @@ export default class Chat extends Component {
|
|||
activeChannelId: parseInt(id, 10),
|
||||
scrolled: false,
|
||||
showAlert: false,
|
||||
unopenedChannelIds: unopenedChannelIds.filter(unopenedId => unopenedId !== id)
|
||||
unopenedChannelIds: unopenedChannelIds.filter(
|
||||
unopenedId => unopenedId !== id,
|
||||
),
|
||||
});
|
||||
this.setupChannel(id);
|
||||
window.history.replaceState(null, null, `/connect/${slug}`);
|
||||
|
|
@ -660,7 +739,9 @@ export default class Chat extends Component {
|
|||
const foundIndex = messages[activeChannelId].findIndex(
|
||||
message => message.temp_id === response.message.temp_id,
|
||||
);
|
||||
newMessages[activeChannelId][foundIndex].id = response.message.id;
|
||||
if (foundIndex > 0) {
|
||||
newMessages[activeChannelId][foundIndex].id = response.message.id;
|
||||
}
|
||||
return { messages: newMessages };
|
||||
});
|
||||
}
|
||||
|
|
@ -681,7 +762,8 @@ export default class Chat extends Component {
|
|||
}
|
||||
|
||||
const { target } = e;
|
||||
const content = target.dataset.content || target.parentElement.dataset.content
|
||||
const content =
|
||||
target.dataset.content || target.parentElement.dataset.content;
|
||||
if (content) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
@ -696,14 +778,18 @@ export default class Chat extends Component {
|
|||
this.setActiveContent,
|
||||
null,
|
||||
);
|
||||
} else if (content.startsWith('sidecar') || content.startsWith('article')) { // article is legacy which can be removed shortly
|
||||
} else if (
|
||||
content.startsWith('sidecar') ||
|
||||
content.startsWith('article')
|
||||
) {
|
||||
// article is legacy which can be removed shortly
|
||||
this.setActiveContentState(activeChannelId, {
|
||||
type_of: 'loading-post',
|
||||
});
|
||||
this.setActiveContent({
|
||||
path: target.href || target.parentElement.href,
|
||||
type_of: 'article'
|
||||
})
|
||||
type_of: 'article',
|
||||
});
|
||||
} else if (target.dataset.content === 'exit') {
|
||||
this.setActiveContentState(activeChannelId, null);
|
||||
}
|
||||
|
|
@ -1098,14 +1184,18 @@ export default class Chat extends Component {
|
|||
<div className="activechatchannel__alerts">
|
||||
<Alert showAlert={state.showAlert} />
|
||||
</div>
|
||||
{this.renderChannelMembersList()}
|
||||
<div className="activechatchannel__form">
|
||||
<Compose
|
||||
handleSubmitOnClick={this.handleSubmitOnClick}
|
||||
handleKeyDown={this.handleKeyDown}
|
||||
handleSubmitOnClickEdit={this.handleSubmitOnClickEdit}
|
||||
handleMention={this.handleMention}
|
||||
handleKeyUp={this.handleKeyUp}
|
||||
handleKeyDownEdit={this.handleKeyDownEdit}
|
||||
activeChannelId={state.activeChannelId}
|
||||
startEditing={state.startEditing}
|
||||
markdownEdited={state.markdownEdited}
|
||||
editMessageHtml={state.activeEditMessage.message}
|
||||
editMessageMarkdown={state.activeEditMessage.markdown}
|
||||
handleEditMessageClose={this.handleEditMessageClose}
|
||||
|
|
@ -1124,11 +1214,170 @@ export default class Chat extends Component {
|
|||
);
|
||||
};
|
||||
|
||||
handleMention = e => {
|
||||
const { activeChannel } = this.state;
|
||||
const mention = e.keyCode === 64;
|
||||
if (mention && activeChannel.channel_type !== 'direct') {
|
||||
this.setState({ showMemberlist: true });
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyUp = e => {
|
||||
const { startEditing, activeChannel, showMemberlist } = this.state;
|
||||
const enterPressed = e.keyCode === 13;
|
||||
if (enterPressed && showMemberlist)
|
||||
this.setState({ showMemberlist: false });
|
||||
if (activeChannel.channel_type !== 'direct') {
|
||||
if (startEditing) {
|
||||
this.setState({ markdownEdited: true });
|
||||
}
|
||||
if (!e.target.value.includes('@')) {
|
||||
this.setState({ showMemberlist: false });
|
||||
} else {
|
||||
this.setQuery(e.target);
|
||||
this.listHighlightManager(e.keyCode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setQuery = e => {
|
||||
const { showMemberlist } = this.state;
|
||||
if (showMemberlist) {
|
||||
const before = e.value.substring(0, e.selectionStart);
|
||||
const query = before.substring(
|
||||
before.lastIndexOf('@') + 1,
|
||||
e.selectionStart,
|
||||
);
|
||||
|
||||
if (query.includes(' ') || before.lastIndexOf('@') < 0)
|
||||
this.setState({ showMemberlist: false });
|
||||
else {
|
||||
this.setState({ showMemberlist: true });
|
||||
this.setState({ memberFilterQuery: query });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
addUserName = e => {
|
||||
const name =
|
||||
e.target.dataset.content || e.target.parentElement.dataset.content;
|
||||
const el = document.getElementById('messageform');
|
||||
const start = el.selectionStart;
|
||||
const end = el.selectionEnd;
|
||||
const text = el.value;
|
||||
let before = text.substring(0, start);
|
||||
before = text.substring(0, before.lastIndexOf('@') + 1);
|
||||
const after = text.substring(end, text.length);
|
||||
el.value = before + name + after;
|
||||
el.selectionStart = start + name.length;
|
||||
el.selectionEnd = start + name.length;
|
||||
el.focus();
|
||||
this.setState({ showMemberlist: false });
|
||||
};
|
||||
|
||||
listHighlightManager = keyCode => {
|
||||
const mentionList = document.getElementById('mentionList');
|
||||
const activeElement = document.querySelector('.active__message__list');
|
||||
if (mentionList.children.length > 0) {
|
||||
if (keyCode === 40 && activeElement) {
|
||||
if (activeElement.nextElementSibling) {
|
||||
activeElement.classList.remove('active__message__list');
|
||||
activeElement.nextElementSibling.classList.add(
|
||||
'active__message__list',
|
||||
);
|
||||
}
|
||||
} else if (keyCode === 38 && activeElement) {
|
||||
if (activeElement.previousElementSibling) {
|
||||
activeElement.classList.remove('active__message__list');
|
||||
activeElement.previousElementSibling.classList.add(
|
||||
'active__message__list',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
mentionList.children[0].classList.add('active__message__list');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
getMentionedUsers = message => {
|
||||
const { channelUsers, activeChannelId, activeChannel } = this.state;
|
||||
if (channelUsers[activeChannelId]) {
|
||||
if (message.includes('@all') && activeChannel.channel_type !== 'open') {
|
||||
return Array.from(
|
||||
Object.values(channelUsers[activeChannelId]).filter(user => user.id),
|
||||
user => user.id,
|
||||
);
|
||||
}
|
||||
return Array.from(
|
||||
Object.values(channelUsers[activeChannelId]).filter(user =>
|
||||
message.includes(user.username),
|
||||
),
|
||||
user => user.id,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
renderChannelMembersList = () => {
|
||||
const {
|
||||
showMemberlist,
|
||||
activeChannelId,
|
||||
channelUsers,
|
||||
memberFilterQuery,
|
||||
} = this.state;
|
||||
const filterRegx = new RegExp(memberFilterQuery, 'gi');
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
showMemberlist ? 'mention__list mention__visible' : 'mention__list'
|
||||
}
|
||||
id="mentionList"
|
||||
>
|
||||
{showMemberlist
|
||||
? Object.values(channelUsers[activeChannelId])
|
||||
.filter(user => user.username.match(filterRegx))
|
||||
.map(user => (
|
||||
<div
|
||||
className="mention__user"
|
||||
role="button"
|
||||
onClick={this.addUserName}
|
||||
tabIndex="0"
|
||||
data-content={user.username}
|
||||
onKeyUp={e => {
|
||||
if (e.keyCode === 13) this.addUserName();
|
||||
}}
|
||||
>
|
||||
<img
|
||||
className="mention__user__image"
|
||||
src={user.profile_image}
|
||||
alt={user.name}
|
||||
style={!user.profile_image ? { display: 'none' } : ' '}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
padding: '3px 0px',
|
||||
'font-size': '16px',
|
||||
}}
|
||||
>
|
||||
{'@'}
|
||||
{user.username}
|
||||
<p>{user.name}</p>
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
: ' '}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
handleEditMessageClose = () => {
|
||||
const textarea = document.getElementById('messageform');
|
||||
this.setState({
|
||||
startEditing: false,
|
||||
markdownEdited: false,
|
||||
activeEditMessage: { message: '', markdown: '' },
|
||||
});
|
||||
textarea.value = '';
|
||||
};
|
||||
|
||||
renderDeleteModal = () => {
|
||||
|
|
@ -1193,7 +1442,7 @@ export default class Chat extends Component {
|
|||
<a
|
||||
href={`/${activeChannel.channel_username}`}
|
||||
onClick={this.triggerActiveContent}
|
||||
data-content='sidecar-user'
|
||||
data-content="sidecar-user"
|
||||
>
|
||||
{activeChannel.channel_modified_slug}
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -7,24 +7,21 @@ export default class Chat extends Component {
|
|||
handleKeyDownEdit: PropTypes.func.isRequired,
|
||||
handleSubmitOnClick: PropTypes.func.isRequired,
|
||||
handleSubmitOnClickEdit: PropTypes.func.isRequired,
|
||||
handleMention: PropTypes.func.isRequired,
|
||||
handleKeyUp: PropTypes.func.isRequired,
|
||||
startEditing: PropTypes.bool.isRequired,
|
||||
markdownEdited: PropTypes.bool.isRequired,
|
||||
editMessageHtml: PropTypes.string.isRequired,
|
||||
editMessageMarkdown: PropTypes.string.isRequired,
|
||||
handleEditMessageClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
editMessageMarkdown: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
this.setState({
|
||||
editMessageMarkdown: props.editMessageMarkdown,
|
||||
editMessageHtml: props.editMessageHtml,
|
||||
});
|
||||
componentDidUpdate() {
|
||||
const { editMessageMarkdown, markdownEdited, startEditing } = this.props;
|
||||
const textarea = document.getElementById('messageform');
|
||||
if (!markdownEdited && startEditing) {
|
||||
textarea.value = editMessageMarkdown;
|
||||
}
|
||||
}
|
||||
|
||||
messageCompose = () => {
|
||||
|
|
@ -32,14 +29,18 @@ export default class Chat extends Component {
|
|||
handleSubmitOnClickEdit,
|
||||
handleKeyDownEdit,
|
||||
handleEditMessageClose,
|
||||
handleMention,
|
||||
handleKeyUp,
|
||||
editMessageHtml,
|
||||
} = this.props;
|
||||
const { editMessageHtml, editMessageMarkdown } = this.state;
|
||||
|
||||
return (
|
||||
<div className="messagecomposer">
|
||||
<div className="messageToBeEdited">
|
||||
<div className="message">
|
||||
<span className="editHead">Edit Message</span>
|
||||
<div
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: editMessageHtml,
|
||||
}}
|
||||
|
|
@ -62,8 +63,9 @@ export default class Chat extends Component {
|
|||
id="messageform"
|
||||
placeholder="Message goes here"
|
||||
onKeyDown={handleKeyDownEdit}
|
||||
onKeyPress={handleMention}
|
||||
onKeyUp={handleKeyUp}
|
||||
maxLength="1000"
|
||||
value={editMessageMarkdown}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -76,30 +78,40 @@ export default class Chat extends Component {
|
|||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { handleSubmitOnClick, handleKeyDown, startEditing } = this.props;
|
||||
textAreaSection = () => {
|
||||
const {
|
||||
handleSubmitOnClick,
|
||||
handleKeyDown,
|
||||
handleMention,
|
||||
handleKeyUp,
|
||||
} = this.props;
|
||||
return (
|
||||
<div>
|
||||
{!startEditing ? (
|
||||
<div className="messagecomposer">
|
||||
<textarea
|
||||
className="messagecomposer__input"
|
||||
id="messageform"
|
||||
placeholder="Message goes here"
|
||||
onKeyDown={handleKeyDown}
|
||||
maxLength="1000"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="messagecomposer__submit"
|
||||
onClick={handleSubmitOnClick}
|
||||
>
|
||||
SEND
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
this.messageCompose()
|
||||
)}
|
||||
<div className="messagecomposer">
|
||||
<textarea
|
||||
className="messagecomposer__input"
|
||||
id="messageform"
|
||||
placeholder="Message goes here"
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyPress={handleMention}
|
||||
onKeyUp={handleKeyUp}
|
||||
maxLength="1000"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="messagecomposer__submit"
|
||||
onClick={handleSubmitOnClick}
|
||||
>
|
||||
SEND
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { startEditing } = this.props;
|
||||
return (
|
||||
<div className="compose__outer__container">
|
||||
{!startEditing ? this.textAreaSection() : this.messageCompose()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,22 +4,18 @@ import setupPusher from './pusher';
|
|||
|
||||
class UnopenedChannelNotice extends Component {
|
||||
propTypes = {
|
||||
unopenedChannels: PropTypes.Object,
|
||||
pusherKey: PropTypes.Object,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
unopenedChannels: undefined,
|
||||
pusherKey: undefined,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { unopenedChannels } = this.props;
|
||||
const visible = unopenedChannels.length > 0;
|
||||
this.state = {
|
||||
visible,
|
||||
unopenedChannels,
|
||||
visible: false,
|
||||
unopenedChannels: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -30,84 +26,165 @@ class UnopenedChannelNotice extends Component {
|
|||
messageCreated: this.receiveNewMessage,
|
||||
messageDeleted: this.removeMessage,
|
||||
messageEdited: this.updateMessage,
|
||||
mentioned: this.mentionedMessage,
|
||||
messageOpened: this.messageOpened,
|
||||
});
|
||||
const component = this;
|
||||
this.fetchUnopenedChannel(this.updateMessageNotification);
|
||||
|
||||
document.getElementById('connect-link').onclick = () => {
|
||||
// Hack, should probably be its own component in future
|
||||
document.getElementById('connect-number').classList.remove('showing');
|
||||
component.setState({ visible: false });
|
||||
this.setState({ visible: false });
|
||||
};
|
||||
}
|
||||
|
||||
updateMessageNotification = unopenedChannels => {
|
||||
const number = document.getElementById('connect-number');
|
||||
this.setState({ unopenedChannels });
|
||||
if (unopenedChannels.length > 0) {
|
||||
number.classList.add('showing');
|
||||
number.innerHTML = unopenedChannels.length;
|
||||
} else {
|
||||
number.classList.remove('showing');
|
||||
}
|
||||
};
|
||||
|
||||
removeMessage = () => {};
|
||||
|
||||
updateMessage = () => {};
|
||||
|
||||
receiveNewMessage = e => {
|
||||
mentionedMessage = e => {
|
||||
if (window.location.pathname.startsWith('/connect')) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(prevState => ({
|
||||
unopenedChannels: prevState.unopenedChannels.map(unopenedChannel =>
|
||||
unopenedChannel.adjusted_slug === e.chat_channel_adjusted_slug
|
||||
? { ...unopenedChannel, request_type: 'mentioned' }
|
||||
: unopenedChannel,
|
||||
),
|
||||
visible: true,
|
||||
}));
|
||||
|
||||
this.hideNotice();
|
||||
};
|
||||
|
||||
messageOpened = e => {
|
||||
const { unopenedChannels } = this.state;
|
||||
if (
|
||||
!window.location.pathname.startsWith('/connect') ||
|
||||
!window.location.pathname.includes(e.adjusted_slug)
|
||||
)
|
||||
return;
|
||||
this.updateMessageNotification(
|
||||
unopenedChannels.filter(
|
||||
unopenedChannel => unopenedChannel.adjusted_slug !== e.adjusted_slug,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
receiveNewMessage = e => {
|
||||
if (
|
||||
(window.location.pathname.startsWith('/connect') &&
|
||||
e.user_id === window.currentUser.id &&
|
||||
e.channel_type !== 'direct') ||
|
||||
window.location.pathname.includes(e.chat_channel_adjusted_slug)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const { unopenedChannels } = this.state;
|
||||
const newObj = { adjusted_slug: e.chat_channel_adjusted_slug };
|
||||
|
||||
const ifMessageExist = unopenedChannels.some(
|
||||
channel => channel.adjusted_slug === newObj.adjusted_slug,
|
||||
);
|
||||
|
||||
if (
|
||||
unopenedChannels.filter(obj => obj.adjusted_slug === newObj.adjusted_slug)
|
||||
.length === 0 &&
|
||||
!ifMessageExist &&
|
||||
newObj.adjusted_slug !== `@${window.currentUser.username}`
|
||||
) {
|
||||
unopenedChannels.push(newObj);
|
||||
}
|
||||
this.setState({
|
||||
visible:
|
||||
unopenedChannels.length > 0 && e.user_id !== window.currentUser.id,
|
||||
unopenedChannels,
|
||||
});
|
||||
|
||||
const number = document.getElementById('connect-number');
|
||||
number.classList.add('showing');
|
||||
number.innerHTML = unopenedChannels.length;
|
||||
const component = this;
|
||||
if (unopenedChannels.length === 0) {
|
||||
number.classList.remove('showing');
|
||||
} else {
|
||||
document.getElementById(
|
||||
'connect-link',
|
||||
).href = `/connect/${unopenedChannels[0].adjusted_slug}`;
|
||||
if (ifMessageExist) {
|
||||
const index = unopenedChannels.findIndex(
|
||||
channel => channel.adjusted_slug === newObj.adjusted_slug,
|
||||
);
|
||||
unopenedChannels[index].notified = false;
|
||||
}
|
||||
setTimeout(() => {
|
||||
component.setState({ visible: false });
|
||||
}, 7500);
|
||||
|
||||
if (!window.location.pathname.startsWith('/connect')) {
|
||||
this.setState({
|
||||
visible:
|
||||
unopenedChannels.length > 0 &&
|
||||
e.user_id !== window.currentUser.id &&
|
||||
e.channel_type === 'direct',
|
||||
});
|
||||
}
|
||||
this.updateMessageNotification(unopenedChannels);
|
||||
this.hideNotice();
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
document.getElementById('connect-number').classList.remove('showing');
|
||||
this.setState({ visible: false });
|
||||
this.hideNotice();
|
||||
};
|
||||
|
||||
hideNotice = () => {
|
||||
setTimeout(() => {
|
||||
this.setState(prevState => ({
|
||||
unopenedChannels: prevState.unopenedChannels.map(unopenedChannel =>
|
||||
!unopenedChannel.notified
|
||||
? { ...unopenedChannel, notified: true }
|
||||
: unopenedChannel,
|
||||
),
|
||||
visible: false,
|
||||
}));
|
||||
}, 7500);
|
||||
};
|
||||
|
||||
fetchUnopenedChannel = successCb => {
|
||||
fetch('/chat_channels?state=unopened', {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(successCb);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { visible, unopenedChannels } = this.state;
|
||||
if (visible) {
|
||||
const channels = unopenedChannels.map(channel => {
|
||||
if (visible && unopenedChannels.some(channel => !channel.notified)) {
|
||||
const message = unopenedChannels.map(channel => {
|
||||
if (channel.notified) return null;
|
||||
return (
|
||||
<a
|
||||
href={`/connect/${unopenedChannels[0].adjusted_slug}`}
|
||||
style={{
|
||||
background: '#66e2d5',
|
||||
color: 'black',
|
||||
border: '1px solid black',
|
||||
padding: '2px 7px',
|
||||
display: 'inline-block',
|
||||
margin: '3px 6px',
|
||||
borderRadius: '3px',
|
||||
}}
|
||||
>
|
||||
{channel.adjusted_slug}
|
||||
</a>
|
||||
<div>
|
||||
{channel.request_type === 'mentioned'
|
||||
? 'You got mentioned in'
|
||||
: 'New Message from'}
|
||||
{' '}
|
||||
<a
|
||||
href={`/connect/${channel.adjusted_slug}`}
|
||||
style={{
|
||||
background: '#66e2d5',
|
||||
color: 'black',
|
||||
border: '1px solid black',
|
||||
padding: '2px 7px',
|
||||
display: 'inline-block',
|
||||
margin: '3px 6px',
|
||||
borderRadius: '3px',
|
||||
}}
|
||||
>
|
||||
{channel.adjusted_slug}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<a
|
||||
onClick={this.handleClick}
|
||||
href={`/connect/${unopenedChannels[0].adjusted_slug}`}
|
||||
onClick={this.handleClick}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
zIndex: '200',
|
||||
|
|
@ -124,9 +201,7 @@ class UnopenedChannelNotice extends Component {
|
|||
padding: '19px 5px 14px',
|
||||
}}
|
||||
>
|
||||
New Message from
|
||||
{' '}
|
||||
{channels}
|
||||
{message}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
|
@ -135,37 +210,12 @@ class UnopenedChannelNotice extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
function manageChannel(json) {
|
||||
const number = document.getElementById('connect-number');
|
||||
if (json.length > 0) {
|
||||
number.classList.add('showing');
|
||||
number.innerHTML = json.length;
|
||||
document.getElementById(
|
||||
'connect-link',
|
||||
).href = `/connect/${json[0].adjusted_slug}`; // Jump the user directly to the channel where appropriate
|
||||
} else {
|
||||
number.classList.remove('showing');
|
||||
}
|
||||
}
|
||||
|
||||
export default function getUnopenedChannels() {
|
||||
if (window.frameElement) {
|
||||
// We don't want this triggered within context of iframe.
|
||||
return;
|
||||
}
|
||||
render(
|
||||
<UnopenedChannelNotice
|
||||
unopenedChannels={[]}
|
||||
pusherKey={document.body.dataset.pusherKey}
|
||||
/>,
|
||||
<UnopenedChannelNotice pusherKey={document.body.dataset.pusherKey} />,
|
||||
document.getElementById('message-notice'),
|
||||
);
|
||||
if (window.location.pathname.startsWith('/connect')) return;
|
||||
fetch('/chat_channels?state=unopened', {
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(json => {
|
||||
manageChannel(json);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ export default function setupPusher(key, callbackObjects) {
|
|||
channel.bind('message-created', callbackObjects.messageCreated);
|
||||
channel.bind('message-deleted', callbackObjects.messageDeleted);
|
||||
channel.bind('message-edited', callbackObjects.messageEdited);
|
||||
channel.bind('mentioned', callbackObjects.mentioned);
|
||||
channel.bind('message-opened', callbackObjects.messageOpened);
|
||||
channel.bind('channel-cleared', callbackObjects.channelCleared);
|
||||
channel.bind('user-banned', callbackObjects.redactUserMessages);
|
||||
channel.bind('client-livecode', callbackObjects.liveCoding);
|
||||
|
|
|
|||
|
|
@ -43,6 +43,10 @@ class ChatChannel < ApplicationRecord
|
|||
channel_type == "invite_only"
|
||||
end
|
||||
|
||||
def group?
|
||||
channel_type != "direct"
|
||||
end
|
||||
|
||||
def clear_channel
|
||||
messages.destroy_all
|
||||
Pusher.trigger(pusher_channels, "channel-cleared", { chat_channel_id: id }.to_json)
|
||||
|
|
@ -112,6 +116,10 @@ class ChatChannel < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def channel_users_ids
|
||||
chat_channel_memberships.pluck(:user_id)
|
||||
end
|
||||
|
||||
def adjusted_slug(user = nil, caller_type = "receiver")
|
||||
user ||= current_user
|
||||
if direct? && caller_type == "receiver"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class Message < ApplicationRecord
|
|||
end
|
||||
|
||||
def direct_receiver
|
||||
return if chat_channel.channel_type != "direct"
|
||||
return if chat_channel.group?
|
||||
|
||||
chat_channel.users.where.not(id: user.id).first
|
||||
end
|
||||
|
|
@ -43,9 +43,54 @@ class Message < ApplicationRecord
|
|||
def evaluate_markdown
|
||||
html = MarkdownParser.new(message_markdown).evaluate_markdown
|
||||
html = append_rich_links(html)
|
||||
html = wrap_mentions_with_links(html)
|
||||
self.message_html = html
|
||||
end
|
||||
|
||||
def wrap_mentions_with_links(html)
|
||||
return unless html
|
||||
|
||||
html_doc = Nokogiri::HTML(html)
|
||||
|
||||
# looks for nodes that isn't <code>, <a>, and contains "@"
|
||||
targets = html_doc.xpath('//html/body/*[not (self::code) and not(self::a) and contains(., "@")]').to_a
|
||||
|
||||
# A Queue system to look for and replace possible usernames
|
||||
until targets.empty?
|
||||
node = targets.shift
|
||||
|
||||
# only focus on portion of text with "@"
|
||||
node.xpath("text()[contains(.,'@')]").each do |el|
|
||||
el.replace(el.text.gsub(/\B@[a-z0-9_-]+/i) { |text| user_link_if_exists(text) })
|
||||
end
|
||||
|
||||
# enqueue children that has @ in it's text
|
||||
children = node.xpath('*[not(self::code) and not(self::a) and contains(., "@")]').to_a
|
||||
targets.concat(children)
|
||||
end
|
||||
|
||||
if html_doc.at_css("body")
|
||||
html_doc.at_css("body").inner_html
|
||||
else
|
||||
html_doc.to_html
|
||||
end
|
||||
end
|
||||
|
||||
def user_link_if_exists(mention)
|
||||
username = mention.delete("@").downcase
|
||||
if User.find_by(username: username) && chat_channel.group?
|
||||
<<~HTML
|
||||
<a class='comment-mentioned-user' data-content="sidecar-user" href='/#{username}' target="_blank">@#{username}</a>
|
||||
HTML
|
||||
elsif username == "all" && chat_channel.channel_type == "invite_only"
|
||||
<<~HTML
|
||||
<a class='comment-mentioned-user comment-mentioned-all' data-content="chat_channels/#{chat_channel.id}" href='#' target="_blank">@#{username}</a>
|
||||
HTML
|
||||
else
|
||||
mention
|
||||
end
|
||||
end
|
||||
|
||||
def append_rich_links(html)
|
||||
doc = Nokogiri::HTML(html)
|
||||
doc.css("a").each do |anchor|
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
--theme-subtle-border: 1px solid #1f2c3a;\
|
||||
--theme-social-icon-invert: invert(100);\
|
||||
--theme-social-icon-color: #cedae2;\
|
||||
--theme-invert-image: invert();\
|
||||
--theme-current-article-background: #1143ca;\
|
||||
--theme-series-article-background: #314d9c;\
|
||||
--theme-series-article-hover: #1b2e63;</style>'
|
||||
|
|
@ -59,6 +60,7 @@
|
|||
--theme-top-bar-write-color: #42ff87;\
|
||||
--theme-container-background: #030303;\
|
||||
--theme-container-accent-background: #000f0b;\
|
||||
--theme-invert-image: invert();\
|
||||
--theme-container-background-hover: #1c2c3f;\
|
||||
--theme-gradient-background: linear-gradient(to right, #010f06 8%, #121a16 18%, #010f06 33%);\
|
||||
--theme-container-color: #42ff87;\
|
||||
|
|
@ -92,6 +94,7 @@
|
|||
--theme-top-bar-write-color: #fff;\
|
||||
--theme-container-background: #fff;\
|
||||
--theme-container-accent-background: #ffccdb;\
|
||||
--theme-invert-image: none;\
|
||||
--theme-container-background-hover: #ffe5eb;\
|
||||
--theme-gradient-background: linear-gradient(to right, #fff 8%, #ffceda 18%, #FFF7F9 33%);\
|
||||
--theme-container-color: #333;\
|
||||
|
|
@ -124,6 +127,7 @@
|
|||
--theme-top-bar-write-color: #0a0a0a;\
|
||||
--theme-container-background: #ffffff;\
|
||||
--theme-container-accent-background: #f4f5f7;\
|
||||
--theme-invert-image: none\
|
||||
--theme-container-background-hover: #efefef;\
|
||||
--theme-gradient-background: linear-gradient(to right, #f6f6f6 8%, #f0f0f0 18%, #f6f6f6 33%);\
|
||||
--theme-container-color: #0a0a0a;\
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ RSpec.describe Message, type: :model do
|
|||
let(:tag) { create(:tag) }
|
||||
let(:chat_channel) { create(:chat_channel) }
|
||||
let(:message) { create(:message, user: user) }
|
||||
let(:random_word) { Faker::Lorem.word }
|
||||
|
||||
describe "validations" do
|
||||
context "with automatic validations" do
|
||||
|
|
@ -71,6 +72,22 @@ RSpec.describe Message, type: :model do
|
|||
|
||||
expect(message.message_html).not_to include("data-content")
|
||||
end
|
||||
|
||||
it "creates mention if user exists" do
|
||||
message.message_markdown = "Hello @#{user.username}"
|
||||
message.validate!
|
||||
|
||||
expect(message.message_html).to include "<a"
|
||||
expect(message.message_html).to include("/#{user.username}")
|
||||
end
|
||||
|
||||
it "doesn't creates mention if user exists" do
|
||||
message.message_markdown = "Hello @#{random_word}"
|
||||
message.validate!
|
||||
|
||||
expect(message.message_html).not_to include "<a"
|
||||
expect(message.message_html).not_to include("/#{random_word}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -201,11 +201,13 @@ RSpec.describe "ChatChannels", type: :request do
|
|||
|
||||
describe "POST /chat_channels/:id/open" do
|
||||
it "returns success" do
|
||||
allow(Pusher).to receive(:trigger).and_return(true)
|
||||
post "/chat_channels/#{chat_channel.id}/open"
|
||||
expect(response.body).to include("success")
|
||||
end
|
||||
|
||||
it "marks chat_channel_membership as opened" do
|
||||
allow(Pusher).to receive(:trigger).and_return(true)
|
||||
post "/chat_channels/#{chat_channel.id}/open"
|
||||
expect(user.chat_channel_memberships.last.has_unopened_messages).to eq(false)
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue