diff --git a/app/assets/stylesheets/chat.scss b/app/assets/stylesheets/chat.scss index b4fc9c4d7..e1a64d109 100644 --- a/app/assets/stylesheets/chat.scss +++ b/app/assets/stylesheets/chat.scss @@ -121,6 +121,21 @@ box-shadow: 3px 3px 0px $dark-purple; } +.chatchanneltabindicator{ + display: inline-block; + background: white; + border: 2px solid $light-medium-gray; + padding: 5px; + border-radius: 100px; + vertical-align: -2px; + margin-right: 3px; +} + +.chatchanneltabindicator--new{ + background: $yellow; + border: 2px solid $black; +} + @keyframes example { // animation-name: example; // animation-duration: 4s; @@ -157,11 +172,11 @@ } .chatmessagebody__profileimage { - height: 18px; - width: 18px; - margin-right: 3px; + height: 23px; + width: 23px; + margin-right: 6px; border-radius: 20px; - vertical-align: bottom; + vertical-align: -5px; } .chatmessagebody__username { diff --git a/app/controllers/chat_channels_controller.rb b/app/controllers/chat_channels_controller.rb index 5ad220d9e..e06f8cc65 100644 --- a/app/controllers/chat_channels_controller.rb +++ b/app/controllers/chat_channels_controller.rb @@ -12,6 +12,13 @@ class ChatChannelsController < ApplicationController end end + def open + @chat_channel = ChatChannel.find(params[:id]) + raise unless @chat_channel.has_member?(current_user) + @chat_channel.chat_channel_memberships.where(user_id: current_user.id).first.touch(:last_opened_at) + render json: { status: "success", channel: params[:id] }, status: 200 + end + def moderate @chat_channel = ChatChannel.find(params[:id]) authorize @chat_channel diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb index 2ca805262..71dc1329e 100644 --- a/app/controllers/pages_controller.rb +++ b/app/controllers/pages_controller.rb @@ -43,7 +43,12 @@ class PagesController < ApplicationController end def chat - @chat_channels = current_user.chat_channels.order("last_message_at DESC") + @chat_channels = current_user.chat_channels. + order("last_message_at DESC"). + includes(:chat_channel_memberships) + @chat_channels.each do |channel| + channel.current_user = current_user + end slug = if params[:slug] && params[:slug].start_with?("@") [current_user.username, params[:slug].gsub("@", "")].sort.join("/") else diff --git a/app/javascript/chat/actions.js b/app/javascript/chat/actions.js index a401540cf..317650759 100644 --- a/app/javascript/chat/actions.js +++ b/app/javascript/chat/actions.js @@ -30,6 +30,22 @@ export function sendMessage(activeChannelId, message, successCb, failureCb) { .catch(failureCb); } +export function sendOpen(activeChannelId, successCb, failureCb) { + fetch(`/chat_channels/${activeChannelId}/open`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'X-CSRF-Token': window.csrfToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + credentials: 'same-origin', + }) + .then(response => response.json()) + .then(successCb) + .catch(failureCb); +} + export function conductModeration( activeChannelId, message, diff --git a/app/javascript/chat/channels.jsx b/app/javascript/chat/channels.jsx index 42aa2d5b7..8babc03fb 100644 --- a/app/javascript/chat/channels.jsx +++ b/app/javascript/chat/channels.jsx @@ -8,6 +8,7 @@ const Channels = ({ activeChannelId, chatChannels, handleSwitchChannel }) => { ? 'chatchanneltab--active' : 'chatchanneltab--inactive'; const name = channel.channel_type === "direct" ? '@'+channel.slug.replace(`${window.currentUser.username}/`, '').replace(`/${window.currentUser.username}`, '') : channel.channel_name + const newMessagesIndicatorClass = new Date(channel.last_opened_at) < new Date(channel.last_message_at) ? "chatchanneltabindicator--new" : "chatchanneltabindicator--old" return ( ); diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx index e9c4ca772..fe0d48eb3 100644 --- a/app/javascript/chat/chat.jsx +++ b/app/javascript/chat/chat.jsx @@ -1,6 +1,6 @@ import { h, Component } from 'preact'; import PropTypes from 'prop-types'; -import { conductModeration, getAllMessages, sendMessage } from './actions'; +import { conductModeration, getAllMessages, sendMessage, sendOpen } from './actions'; import { hideMessages, scrollToBottom, setupObserver } from './util'; import Alert from './alert'; import Channels from './channels'; @@ -54,6 +54,11 @@ export default class Chat extends Component { channelCleared: this.clearChannel, redactUserMessages: this.redactUserMessages, }); + sendOpen( + this.state.activeChannelId, + this.handleChannelOpenSuccess, + null, + ); }; observerCallback = entries => { @@ -83,8 +88,22 @@ export default class Chat extends Component { this.state.activeChannelId === receivedChatChannelId ? { showAlert: this.state.scrolled } : {}; + const newChannelsObj = this.state.chatChannels.map(channel => { + if (receivedChatChannelId === channel["id"]){ + channel["last_message_at"] = new Date(); + } + return channel; + }); + if (receivedChatChannelId === this.state.activeChannelId) { + sendOpen( + receivedChatChannelId, + this.handleChannelOpenSuccess, + null, + ); + } this.setState({ ...newShowAlert, + chatChannels: newChannelsObj, messages: { ...this.state.messages, [receivedChatChannelId]: newMessages, @@ -148,6 +167,11 @@ export default class Chat extends Component { showAlert: false, }); window.history.replaceState(null, null, "/chat/"+e.target.dataset.channelName); + sendOpen( + e.target.dataset.channelId, + this.handleChannelOpenSuccess, + null, + ); }; handleSubmitOnClick = e => { @@ -165,6 +189,16 @@ export default class Chat extends Component { } }; + handleChannelOpenSuccess = response => { + const newChannelsObj = this.state.chatChannels.map(channel => { + if (parseInt(response.channel) === channel["id"]){ + channel["last_opened_at"] = new Date(); + } + return channel; + }); + this.setState({ chatChannels: newChannelsObj }); + }; + handleFailure = err => { console.error(err); }; @@ -217,6 +251,8 @@ export default class Chat extends Component { ); + + render() { return (
diff --git a/app/models/chat_channel.rb b/app/models/chat_channel.rb index 6f6b1a9a1..bef214970 100644 --- a/app/models/chat_channel.rb +++ b/app/models/chat_channel.rb @@ -1,4 +1,6 @@ class ChatChannel < ApplicationRecord + attr_accessor :current_user + has_many :messages has_many :chat_channel_memberships has_many :users, through: :chat_channel_memberships @@ -18,6 +20,11 @@ class ChatChannel < ApplicationRecord users.include?(user) end + def last_opened_at(user = nil) + user ||= current_user + chat_channel_memberships.where(user_id: user.id).pluck(:last_opened_at).first + end + def self.create_with_users(users, channel_type = "direct", contrived_name = "New Channel") raise "Invalid direct channel" if users.size != 2 && channel_type == "direct" if channel_type == "direct" diff --git a/app/views/pages/chat.html.erb b/app/views/pages/chat.html.erb index 4274d807c..37b99ebd4 100644 --- a/app/views/pages/chat.html.erb +++ b/app/views/pages/chat.html.erb @@ -24,7 +24,7 @@ id="chat" class="live-chat" data-pusher-key="<%= ENV["PUSHER_KEY"] %>" - data-chat-channels="<%= @chat_channels.to_json %>" + data-chat-channels="<%= @chat_channels.to_json(methods: :last_opened_at) %>" data-chat-options="<%= {showChannelsList:true, showTimestamp: true, activeChannelId: @active_channel.id }.to_json %>">
<% else %> diff --git a/config/routes.rb b/config/routes.rb index 15054a754..fb3ae813a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -98,6 +98,7 @@ Rails.application.routes.draw do patch "/onboarding_update" => "users#onboarding_update" get "email_subscriptions/unsubscribe" post "chat_channels/:id/moderate" => "chat_channels#moderate" + post "chat_channels/:id/open" => "chat_channels#open" # resources :users get "/social_previews/article/:id" => "social_previews#article"