Progress with DMs

Progress with DMs
This commit is contained in:
mgabdev 2020-12-03 17:13:11 -05:00
parent a129b3ce3b
commit 137a36b810
53 changed files with 539 additions and 182 deletions

View File

@ -70,12 +70,7 @@ module Admin
end end
def filter_params def filter_params
params.permit( params.permit(:shortcode)
:local,
:remote,
:by_domain,
:shortcode
)
end end
end end
end end

View File

@ -26,7 +26,7 @@ class Api::V1::ChatConversationController < Api::BaseController
end end
def mark_chat_conversation_unread def mark_chat_conversation_unread
@chat_conversation_account.update!(is_unread: true) @chat_conversation_account.update!(unread_count: 1)
render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer render json: @chat_conversation_account, serializer: REST::ChatConversationAccountSerializer
end end

View File

@ -4,22 +4,30 @@ class Api::V1::ChatConversations::ApprovedConversationsController < Api::BaseCon
before_action -> { authorize_if_got_token! :read, :'read:chats' } before_action -> { authorize_if_got_token! :read, :'read:chats' }
before_action :require_user! before_action :require_user!
before_action :set_chat_conversation, only: :create
after_action :insert_pagination_headers after_action :insert_pagination_headers
def index def index
puts "tilly ApprovedConversationsController-0"
@chat_conversations = load_chat_conversations @chat_conversations = load_chat_conversations
render json: @chat_conversations, each_serializer: REST::ChatConversationAccountSerializer render json: @chat_conversations, each_serializer: REST::ChatConversationAccountSerializer
end end
def show def show
puts "tilly ApprovedConversationsController-1" render json: @chat_conversation, serializer: REST::ChatConversationAccountSerializer
@chat_conversations = load_chat_conversations end
render json: @chat_conversations, each_serializer: REST::ChatConversationAccountSerializer
def unread_count
# : todo : make is_unread into unread_count then count
# count = ChatConversationAccount.where(account: current_account, is_hidden: false, is_approved: true, unread_count: true).count
render json: 1
end end
private private
def set_chat_conversation
@chat_conversation = ChatConversationAccount.where(account: current_account).find(params[:id]).first
end
def load_chat_conversations def load_chat_conversations
paginated_chat_conversations paginated_chat_conversations
end end

View File

@ -15,6 +15,22 @@ class Api::V1::ChatConversations::MessagesController < Api::BaseController
render json: @chats, each_serializer: REST::ChatMessageSerializer render json: @chats, each_serializer: REST::ChatMessageSerializer
end end
def destroy_all
puts "tilly destry all chat"
# : todo :
# check if is pro
# @chat = ChatMessage.where(from_account: current_user.account).find(params[:id])
puts "tilly @chat: " + @chat.inspect
# : todo :
# make sure last_chat_message_id in chat_account_conversation gets set to last
# @chat.destroy!
# render json: @chat, serializer: REST::ChatMessageSerializer
end
private private
def set_chat_conversation def set_chat_conversation

View File

@ -5,25 +5,33 @@ class Api::V1::ChatMessagesController < Api::BaseController
before_action -> { doorkeeper_authorize! :write, :'write:chats' } before_action -> { doorkeeper_authorize! :write, :'write:chats' }
before_action :require_user! before_action :require_user!
before_action :set_chat_conversation before_action :set_chat_conversation, only: :create
before_action :set_chat_conversation_recipients, only: :create
def create def create
@chat = ChatMessage.create!( @chat = ChatMessage.create!(
from_account: current_account, from_account: current_account,
chat_conversation: @chat_conversation, chat_conversation: @chat_conversation,
text: params[:text] text: ActionController::Base.helpers.strip_tags(params[:text])
) )
# : todo : # : todo :
# Redis.current.publish("chat_messages:10", 'hi') # check if blocked
Redis.current.publish("chat_messages:10", Oj.dump(event: :chat_message, payload: InlineRenderer.render(@chat, current_user.account, :chat_message)))
@chat_conversation_recipients.each do |account|
payload = InlineRenderer.render(@chat, account, :chat_message)
Redis.current.publish("chat_messages:#{account.id}", Oj.dump(event: :notification, payload: payload))
end
render json: @chat, serializer: REST::ChatMessageSerializer render json: @chat, serializer: REST::ChatMessageSerializer
end end
def destroy def destroy
@chat = ChatMessage.where(account: current_user.account).find(params[:id]) puts "tilly destry chat"
authorize @chat, :destroy?
@chat = ChatMessage.where(from_account: current_user.account).find(params[:id])
puts "tilly @chat: " + @chat.inspect
# : todo : # : todo :
# make sure last_chat_message_id in chat_account_conversation gets set to last # make sure last_chat_message_id in chat_account_conversation gets set to last
@ -39,6 +47,12 @@ class Api::V1::ChatMessagesController < Api::BaseController
@chat_conversation = ChatConversation.find(params[:chat_conversation_id]) @chat_conversation = ChatConversation.find(params[:chat_conversation_id])
end end
def set_chat_conversation_recipients
account_conversation = ChatConversationAccount.where(account: current_user.account, chat_conversation: @chat_conversation).first
puts "tilly account_conversation - " + account_conversation.inspect
@chat_conversation_recipients = Account.where(id: account_conversation.participant_account_ids)
end
def chat_params def chat_params
params.permit(:text, :chat_conversation_id) params.permit(:text, :chat_conversation_id)
end end

View File

@ -13,6 +13,8 @@ export const CHAT_CONVERSATIONS_APPROVED_EXPAND_REQUEST = 'CHAT_CONVERSATIONS_AP
export const CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS = 'CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS' export const CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS = 'CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS'
export const CHAT_CONVERSATIONS_APPROVED_EXPAND_FAIL = 'CHAT_CONVERSATIONS_APPROVED_EXPAND_FAIL' export const CHAT_CONVERSATIONS_APPROVED_EXPAND_FAIL = 'CHAT_CONVERSATIONS_APPROVED_EXPAND_FAIL'
export const CHAT_CONVERSATION_APPROVED_UNREAD_COUNT_FETCH_SUCCESS = 'CHAT_CONVERSATIONS_APPROVED_EXPAND_FAIL'
// //
export const CHAT_CONVERSATIONS_CREATE_REQUEST = 'CHAT_CONVERSATIONS_CREATE_REQUEST' export const CHAT_CONVERSATIONS_CREATE_REQUEST = 'CHAT_CONVERSATIONS_CREATE_REQUEST'
@ -437,6 +439,20 @@ export const fetchChatConversationRequestedCount = () => (dispatch, getState) =>
}) })
} }
/**
*
*/
export const fetchChatConversationUnreadCount = () => (dispatch, getState) => {
if (!me) return
api(getState).get('/api/v1/chat_conversations/approved_conversations/unread_count').then(response => {
dispatch({
type: CHAT_CONVERSATION_APPROVED_UNREAD_COUNT_FETCH_SUCCESS,
count: response.data,
})
})
}
/** /**
* *
*/ */

View File

@ -19,7 +19,7 @@ export const sendChatMessage = (text = '', chatConversationId) => (dispatch, get
if (!me || !chatConversationId) return if (!me || !chatConversationId) return
if (text.length === 0) return if (text.length === 0) return
dispatch(sendMessageRequest()) dispatch(sendChatMessageRequest(chatConversationId))
api(getState).post('/api/v1/chat_messages', { api(getState).post('/api/v1/chat_messages', {
text, text,
@ -30,23 +30,23 @@ export const sendChatMessage = (text = '', chatConversationId) => (dispatch, get
// }, // },
}).then((response) => { }).then((response) => {
dispatch(importFetchedChatMessages([response.data])) dispatch(importFetchedChatMessages([response.data]))
dispatch(sendMessageSuccess(response.data, chatConversationId)) dispatch(sendChatMessageSuccess(response.data))
}).catch((error) => { }).catch((error) => {
dispatch(sendMessageFail(error)) dispatch(sendChatMessageFail(error))
}) })
} }
const sendMessageRequest = () => ({ const sendChatMessageRequest = (chatConversationId) => ({
type: CHAT_MESSAGES_SEND_REQUEST, type: CHAT_MESSAGES_SEND_REQUEST,
})
const sendMessageSuccess = (chatMessage, chatConversationId) => ({
type: CHAT_MESSAGES_SEND_SUCCESS,
chatMessage,
chatConversationId, chatConversationId,
}) })
const sendMessageFail = (error) => ({ export const sendChatMessageSuccess = (chatMessage) => ({
type: CHAT_MESSAGES_SEND_SUCCESS,
chatMessage,
})
const sendChatMessageFail = (error) => ({
type: CHAT_MESSAGES_SEND_FAIL, type: CHAT_MESSAGES_SEND_FAIL,
error, error,
}) })
@ -54,32 +54,32 @@ const sendMessageFail = (error) => ({
/** /**
* *
*/ */
const deleteMessage = (chatMessageId) => (dispatch, getState) => { export const deleteChatMessage = (chatMessageId) => (dispatch, getState) => {
if (!me || !chatMessageId) return if (!me || !chatMessageId) return
dispatch(deleteMessageRequest(chatMessageId)) dispatch(deleteChatMessageRequest(chatMessageId))
api(getState).delete(`/api/v1/chat_messages/${chatMessageId}`, {}, { api(getState).delete(`/api/v1/chat_messages/${chatMessageId}`, {}, {
// headers: { // headers: {
// 'Idempotency-Key': getState().getIn(['chat_compose', 'idempotencyKey']), // 'Idempotency-Key': getState().getIn(['chat_compose', 'idempotencyKey']),
// }, // },
}).then((response) => { }).then((response) => {
deleteMessageSuccess(response) dispatch(deleteChatMessageSuccess(response.data))
}).catch((error) => { }).catch((error) => {
dispatch(deleteMessageFail(error)) dispatch(deleteChatMessageFail(error))
}) })
} }
const deleteMessageRequest = (chatMessageId) => ({ const deleteChatMessageRequest = (chatMessageId) => ({
type: CHAT_MESSAGES_DELETE_REQUEST, type: CHAT_MESSAGES_DELETE_REQUEST,
chatMessageId, chatMessageId,
}) })
const deleteMessageSuccess = () => ({ const deleteChatMessageSuccess = () => ({
type: CHAT_MESSAGES_DELETE_SUCCESS, type: CHAT_MESSAGES_DELETE_SUCCESS,
}) })
const deleteMessageFail = (error) => ({ const deleteChatMessageFail = (error) => ({
type: CHAT_MESSAGES_DELETE_FAIL, type: CHAT_MESSAGES_DELETE_FAIL,
error, error,
}) })

View File

@ -6,6 +6,7 @@ import {
updateTimelineQueue, updateTimelineQueue,
} from './timelines' } from './timelines'
import { updateNotificationsQueue } from './notifications' import { updateNotificationsQueue } from './notifications'
import { sendChatMessageSuccess } from './chat_messages'
import { fetchFilters } from './filters' import { fetchFilters } from './filters'
import { getLocale } from '../locales' import { getLocale } from '../locales'
import { handleComposeSubmit } from './compose' import { handleComposeSubmit } from './compose'
@ -76,17 +77,15 @@ export const connectUserStream = () => connectTimelineStream('home', 'user')
* *
*/ */
export const connectChatMessagesStream = (accountId) => { export const connectChatMessagesStream = (accountId) => {
return connectStream(`chat_messages:${accountId}`, null, (dispatch, getState) => { return connectStream(`chat_messages`, null, (dispatch, getState) => {
return { return {
onConnect() { onConnect() {},
// console.log("chat messages connected") onDisconnect() {},
},
onDisconnect() {
// console.log("chat messages disconnected")
},
onReceive (data) { onReceive (data) {
// : todo : if (!data['event'] || !data['payload']) return
console.log("chat messages onReceive:", data) if (data.event === 'notification') {
dispatch(sendChatMessageSuccess(JSON.parse(data.payload)))
}
}, },
} }
}) })

View File

@ -81,6 +81,7 @@ class ListItem extends React.PureComponent {
const textContainerClasses = CX({ const textContainerClasses = CX({
d: 1, d: 1,
pr5: 1, pr5: 1,
w100PC: hideArrow,
maxW100PC42PX: !hideArrow || showActive, maxW100PC42PX: !hideArrow || showActive,
}) })

View File

@ -67,6 +67,8 @@ class DefaultNavigationBar extends ImmutablePureComponent {
account, account,
noActions, noActions,
logoDisabled, logoDisabled,
unreadChatsCount,
notificationCount,
} = this.props } = this.props
const navigationContainerClasses = CX({ const navigationContainerClasses = CX({
@ -171,7 +173,8 @@ class DefaultNavigationBar extends ImmutablePureComponent {
<div className={[_s.d, _s.h20PX, _s.w1PX, _s.mr10, _s.ml10, _s.bgNavigationBlend].join(' ')} /> <div className={[_s.d, _s.h20PX, _s.w1PX, _s.mr10, _s.ml10, _s.bgNavigationBlend].join(' ')} />
<NavigationBarButton attrTitle='Notifications' icon='notifications' to='/notifications' /> <NavigationBarButton attrTitle='Notifications' icon='notifications' to='/notifications' count={notificationCount} />
<NavigationBarButton attrTitle='Chats' icon='chat' to='/messages' count={unreadChatsCount} />
<NavigationBarButton attrTitle='Dark/Muted/Light/White Mode' icon='light-bulb' onClick={this.handleOnClickLightBulb} /> <NavigationBarButton attrTitle='Dark/Muted/Light/White Mode' icon='light-bulb' onClick={this.handleOnClickLightBulb} />
<div className={[_s.d, _s.h20PX, _s.w1PX, _s.mr10, _s.ml10, _s.bgNavigationBlend].join(' ')} /> <div className={[_s.d, _s.h20PX, _s.w1PX, _s.mr10, _s.ml10, _s.bgNavigationBlend].join(' ')} />
@ -236,6 +239,7 @@ class DefaultNavigationBar extends ImmutablePureComponent {
attrTitle={action.attrTitle} attrTitle={action.attrTitle}
title={action.title} title={action.title}
icon={action.icon} icon={action.icon}
count={action.count}
to={action.to || undefined} to={action.to || undefined}
onClick={action.onClick ? () => action.onClick() : undefined} onClick={action.onClick ? () => action.onClick() : undefined}
key={`action-btn-${i}`} key={`action-btn-${i}`}
@ -261,6 +265,8 @@ const mapStateToProps = (state) => ({
emailConfirmationResends: state.getIn(['user', 'emailConfirmationResends'], 0), emailConfirmationResends: state.getIn(['user', 'emailConfirmationResends'], 0),
theme: state.getIn(['settings', 'displayOptions', 'theme'], DEFAULT_THEME), theme: state.getIn(['settings', 'displayOptions', 'theme'], DEFAULT_THEME),
logoDisabled: state.getIn(['settings', 'displayOptions', 'logoDisabled'], false), logoDisabled: state.getIn(['settings', 'displayOptions', 'logoDisabled'], false),
notificationCount: state.getIn(['notifications', 'unread']),
unreadChatsCount: state.getIn(['chats', 'chatsUnreadCount']),
}) })
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
@ -288,6 +294,8 @@ DefaultNavigationBar.propTypes = {
tabs: PropTypes.array, tabs: PropTypes.array,
title: PropTypes.string, title: PropTypes.string,
showBackBtn: PropTypes.bool, showBackBtn: PropTypes.bool,
notificationCount: PropTypes.number.isRequired,
unreadChatsCount: PropTypes.number.isRequired,
onOpenNavSettingsPopover: PropTypes.func.isRequired, onOpenNavSettingsPopover: PropTypes.func.isRequired,
onOpenEmailModal: PropTypes.func.isRequired, onOpenEmailModal: PropTypes.func.isRequired,
onResendUserConfirmationEmail: PropTypes.func.isRequired, onResendUserConfirmationEmail: PropTypes.func.isRequired,

View File

@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { shortNumberFormat } from '../utils/numbers'
import { CX } from '../constants' import { CX } from '../constants'
import Button from './button' import Button from './button'
import Icon from './icon' import Icon from './icon'
@ -53,17 +54,22 @@ class NavigationBarButton extends React.PureComponent {
const countClasses = CX({ const countClasses = CX({
d: 1, d: 1,
text: 1,
textAlignCenter: 1,
minW20PX: 1,
mlAuto: 1,
fs12PX: 1, fs12PX: 1,
posAbs: 1, px5: 1,
mr2: 1,
lineHeight15: 1,
ml5: 1,
cWhite: 1,
rightNeg5PX: 1,
top0: 1, top0: 1,
mt15: 1, mt5: 1,
right0: 1, bgRed: 1,
mr5: 1, posAbs: 1,
h4PX: 1, circle: 1,
w4PX: 1,
cBrand: 1,
bgNavigationBlend: 1,
radiusSmall: 1,
}) })
const iconContainerClasses = CX({ const iconContainerClasses = CX({
@ -112,7 +118,9 @@ class NavigationBarButton extends React.PureComponent {
} }
{ {
!title && count > 0 && !title && count > 0 &&
<span className={countClasses} /> <span className={countClasses}>
{shortNumberFormat(count)}
</span>
} }
</Button> </Button>
) )

View File

@ -0,0 +1,113 @@
import React from 'react'
import PropTypes from 'prop-types'
import ImmutablePropTypes from 'react-immutable-proptypes'
import ImmutablePureComponent from 'react-immutable-pure-component'
import { connect } from 'react-redux'
import { closePopover } from '../../actions/popover'
import { openModal } from '../../actions/modal'
import { MODAL_PRO_UPGRADE } from '../../constants'
import { me } from '../../initial_state'
import { makeGetChatConversation } from '../../selectors'
import { changeSetting, saveSettings } from '../../actions/settings'
import PopoverLayout from './popover_layout'
import List from '../list'
class ChatConversationOptionsPopover extends ImmutablePureComponent {
handleOnBlock = () => {
//
}
handleOnHide = () => {
}
handleOnMute = () => {
}
handleOnPurge = () => {
if (!this.props.isPro) {
this.props.openProUpgradeModal()
} else {
//
}
}
handleOnClosePopover = () => {
this.props.onClosePopover()
}
render() {
const {
intl,
isXS,
} = this.props
const items = [
{
hideArrow: true,
title: 'Block Messenger',
subtitle: 'The messenger will not be able to message you.',
onClick: () => this.handleOnBlock(),
},
{
hideArrow: true,
title: 'Mute Messenger',
subtitle: 'You will not be notified of new messsages',
onClick: () => this.handleOnMute(),
},
{
hideArrow: true,
title: 'Hide Conversation',
subtitle: 'Hide until next message',
onClick: () => this.handleOnHide(),
},
{},
{
hideArrow: true,
title: 'Purge messages',
subtitle: 'Remove all of your messages in this conversation',
onClick: () => this.handleOnPurge(),
},
]
return (
<PopoverLayout
width={220}
isXS={isXS}
onClose={this.handleOnClosePopover}
>
<List
size={isXS ? 'large' : 'small'}
scrollKey='chat_conversation_options'
items={items}
/>
</PopoverLayout>
)
}
}
const mapStateToProps = (state, { chatConversationId }) => ({
isPro: state.getIn(['accounts', me, 'is_pro']),
chatConversation: makeGetChatConversation()(state, { id: chatConversationId }),
})
const mapDispatchToProps = (dispatch) => ({
openProUpgradeModal() {
dispatch(openModal(MODAL_PRO_UPGRADE))
},
onSetCommentSortingSetting(type) {
dispatch(closePopover())
},
onClosePopover: () => dispatch(closePopover()),
})
ChatConversationOptionsPopover.propTypes = {
isXS: PropTypes.bool,
isPro: PropTypes.bool.isRequired,
chatConversation: ImmutablePropTypes.map,
onClosePopover: PropTypes.func.isRequired,
}
export default connect(mapStateToProps, mapDispatchToProps)(ChatConversationOptionsPopover)

View File

@ -0,0 +1,54 @@
import React from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import { closePopover } from '../../actions/popover'
import { deleteChatMessage } from '../../actions/chat_messages'
import PopoverLayout from './popover_layout'
import Button from '../button'
import Text from '../text'
class ChatMessageDeletePopover extends React.PureComponent {
handleOnClick = () => {
this.props.onDeleteChatMessage(this.props.chatMessageId)
}
handleOnClosePopover = () => {
this.props.onClosePopover()
}
render() {
const { isXS } = this.props
return (
<PopoverLayout
width={96}
isXS={isXS}
onClose={this.handleOnClosePopover}
>
<Button
onClick={this.handleOnClick}
color='primary'
backgroundColor='tertiary'
className={[_s.radiusSmall].join(' ')}
>
<Text align='center' color='inherit'>Remove</Text>
</Button>
</PopoverLayout>
)
}
}
const mapDispatchToProps = (dispatch) => ({
onDeleteChatMessage(chatMessageId) {
dispatch(deleteChatMessage(chatMessageId))
},
})
ChatMessageDeletePopover.propTypes = {
isXS: PropTypes.bool,
chatMessageId: PropTypes.string.isRequired,
onDeleteChatMessage: PropTypes.func.isRequired,
}
export default connect(null, mapDispatchToProps)(ChatMessageDeletePopover)

View File

@ -1,5 +1,7 @@
import { import {
BREAKPOINT_EXTRA_SMALL, BREAKPOINT_EXTRA_SMALL,
POPOVER_CHAT_CONVERSATION_OPTIONS,
POPOVER_CHAT_MESSAGE_DELETE,
POPOVER_COMMENT_SORTING_OPTIONS, POPOVER_COMMENT_SORTING_OPTIONS,
POPOVER_DATE_PICKER, POPOVER_DATE_PICKER,
POPOVER_EMOJI_PICKER, POPOVER_EMOJI_PICKER,
@ -20,6 +22,8 @@ import {
POPOVER_VIDEO_STATS, POPOVER_VIDEO_STATS,
} from '../../constants' } from '../../constants'
import { import {
ChatConversationOptionsPopover,
ChatMessageDeletePopover,
CommentSortingOptionsPopover, CommentSortingOptionsPopover,
DatePickerPopover, DatePickerPopover,
EmojiPickerPopover, EmojiPickerPopover,
@ -53,25 +57,28 @@ import LoadingPopover from './loading_popover'
const initialState = getWindowDimension() const initialState = getWindowDimension()
const POPOVER_COMPONENTS = {} const POPOVER_COMPONENTS = {
POPOVER_COMPONENTS[POPOVER_COMMENT_SORTING_OPTIONS] = CommentSortingOptionsPopover [POPOVER_CHAT_CONVERSATION_OPTIONS]: ChatConversationOptionsPopover,
POPOVER_COMPONENTS[POPOVER_DATE_PICKER] = DatePickerPopover [POPOVER_CHAT_MESSAGE_DELETE]: ChatMessageDeletePopover,
POPOVER_COMPONENTS[POPOVER_EMOJI_PICKER] = EmojiPickerPopover [POPOVER_COMMENT_SORTING_OPTIONS]: CommentSortingOptionsPopover,
POPOVER_COMPONENTS[POPOVER_GROUP_LIST_SORT_OPTIONS] = GroupListSortOptionsPopover [POPOVER_DATE_PICKER]: DatePickerPopover,
POPOVER_COMPONENTS[POPOVER_GROUP_MEMBER_OPTIONS] = GroupMemberOptionsPopover [POPOVER_EMOJI_PICKER]: EmojiPickerPopover,
POPOVER_COMPONENTS[POPOVER_GROUP_OPTIONS] = GroupOptionsPopover [POPOVER_GROUP_LIST_SORT_OPTIONS]: GroupListSortOptionsPopover,
POPOVER_COMPONENTS[POPOVER_GROUP_TIMELINE_SORT_OPTIONS] = GroupTimelineSortOptionsPopover [POPOVER_GROUP_MEMBER_OPTIONS]: GroupMemberOptionsPopover,
POPOVER_COMPONENTS[POPOVER_GROUP_TIMELINE_SORT_TOP_OPTIONS] = GroupTimelineSortTopOptionsPopover [POPOVER_GROUP_OPTIONS]: GroupOptionsPopover,
POPOVER_COMPONENTS[POPOVER_NAV_SETTINGS] = NavSettingsPopover [POPOVER_GROUP_TIMELINE_SORT_OPTIONS]: GroupTimelineSortOptionsPopover,
POPOVER_COMPONENTS[POPOVER_PROFILE_OPTIONS] = ProfileOptionsPopover [POPOVER_GROUP_TIMELINE_SORT_TOP_OPTIONS]: GroupTimelineSortTopOptionsPopover,
POPOVER_COMPONENTS[POPOVER_SIDEBAR_MORE] = SidebarMorePopover [POPOVER_NAV_SETTINGS]: NavSettingsPopover,
POPOVER_COMPONENTS[POPOVER_STATUS_OPTIONS] = StatusOptionsPopover [POPOVER_PROFILE_OPTIONS]: ProfileOptionsPopover,
POPOVER_COMPONENTS[POPOVER_STATUS_EXPIRATION_OPTIONS] = StatusExpirationOptionsPopover [POPOVER_SIDEBAR_MORE]: SidebarMorePopover,
POPOVER_COMPONENTS[POPOVER_STATUS_SHARE] = StatusSharePopover [POPOVER_STATUS_OPTIONS]: StatusOptionsPopover,
POPOVER_COMPONENTS[POPOVER_STATUS_VISIBILITY] = StatusVisibilityPopover [POPOVER_STATUS_EXPIRATION_OPTIONS]: StatusExpirationOptionsPopover,
POPOVER_COMPONENTS[POPOVER_TIMELINE_INJECTION_OPTIONS] = TimelineInjectionOptionsPopover [POPOVER_STATUS_SHARE]: StatusSharePopover,
POPOVER_COMPONENTS[POPOVER_USER_INFO] = UserInfoPopover [POPOVER_STATUS_VISIBILITY]: StatusVisibilityPopover,
POPOVER_COMPONENTS[POPOVER_VIDEO_STATS] = VideoStatsPopover [POPOVER_TIMELINE_INJECTION_OPTIONS]: TimelineInjectionOptionsPopover,
[POPOVER_USER_INFO]: UserInfoPopover,
[POPOVER_VIDEO_STATS]: VideoStatsPopover,
}
class PopoverRoot extends React.PureComponent { class PopoverRoot extends React.PureComponent {

View File

@ -54,6 +54,7 @@ class DefaultSidebar extends ImmutablePureComponent {
title, title,
showBackBtn, showBackBtn,
shortcuts, shortcuts,
unreadChatsCount,
} = this.props } = this.props
const { hoveringShortcuts } = this.state const { hoveringShortcuts } = this.state
@ -82,7 +83,7 @@ class DefaultSidebar extends ImmutablePureComponent {
</SidebarSectionTitle> </SidebarSectionTitle>
<SidebarSectionItem title='Home' icon='home' to='/home' count={homeItemsQueueCount} /> <SidebarSectionItem title='Home' icon='home' to='/home' count={homeItemsQueueCount} />
<SidebarSectionItem title='Notifications' icon='notifications' to='/notifications' count={notificationCount} /> <SidebarSectionItem title='Notifications' icon='notifications' to='/notifications' count={notificationCount} />
<SidebarSectionItem title='Chats' icon='chat' to='/messages' /> <SidebarSectionItem title='Chats' icon='chat' to='/messages' count={unreadChatsCount} />
<SidebarSectionItem title='Groups' icon='group' to='/groups' /> <SidebarSectionItem title='Groups' icon='group' to='/groups' />
<SidebarSectionItem title='Lists' icon='list' to='/lists' /> <SidebarSectionItem title='Lists' icon='list' to='/lists' />
<SidebarSectionItem title='Explore' icon='explore' to='/explore' /> <SidebarSectionItem title='Explore' icon='explore' to='/explore' />
@ -151,6 +152,7 @@ const mapStateToProps = (state) => ({
shortcuts: state.getIn(['shortcuts', 'items']), shortcuts: state.getIn(['shortcuts', 'items']),
moreOpen: state.getIn(['popover', 'popoverType']) === 'SIDEBAR_MORE', moreOpen: state.getIn(['popover', 'popoverType']) === 'SIDEBAR_MORE',
notificationCount: state.getIn(['notifications', 'unread']), notificationCount: state.getIn(['notifications', 'unread']),
unreadChatsCount: state.getIn(['chats', 'chatsUnreadCount']),
homeItemsQueueCount: state.getIn(['timelines', 'home', 'totalQueuedItemsCount']), homeItemsQueueCount: state.getIn(['timelines', 'home', 'totalQueuedItemsCount']),
}) })
@ -170,6 +172,7 @@ DefaultSidebar.propTypes = {
openSidebarMorePopover: PropTypes.func.isRequired, openSidebarMorePopover: PropTypes.func.isRequired,
notificationCount: PropTypes.number.isRequired, notificationCount: PropTypes.number.isRequired,
homeItemsQueueCount: PropTypes.number.isRequired, homeItemsQueueCount: PropTypes.number.isRequired,
unreadChatsCount: PropTypes.number.isRequired,
actions: PropTypes.array, actions: PropTypes.array,
tabs: PropTypes.array, tabs: PropTypes.array,
title: PropTypes.string, title: PropTypes.string,

View File

@ -45,7 +45,7 @@ class SidebarSectionItem extends React.PureComponent {
const iconSize = '16px' const iconSize = '16px'
const currentPathname = noRouter ? '' : this.context.router.route.location.pathname const currentPathname = noRouter ? '' : this.context.router.route.location.pathname
const shouldShowActive = hovering || active || currentPathname === to || currentPathname === href const shouldShowActive = hovering || active || currentPathname === to || currentPathname === href
const isNotifications = to === '/notifications' const isHighlighted = ['/notifications', '/messages'].indexOf(to) > -1
const containerClasses = CX({ const containerClasses = CX({
d: 1, d: 1,
@ -67,16 +67,18 @@ class SidebarSectionItem extends React.PureComponent {
const countClasses = CX({ const countClasses = CX({
d: 1, d: 1,
text: 1, text: 1,
textAlignCenter: 1,
minW20PX: 1,
mlAuto: 1, mlAuto: 1,
fs12PX: 1, fs12PX: 1,
px5: 1, px5: 1,
mr2: 1, mr2: 1,
lineHeight15: 1, lineHeight15: 1,
ml5: 1, ml5: 1,
cSecondary: !isNotifications, cSecondary: !isHighlighted,
cWhite: isNotifications, cWhite: isHighlighted,
bgBrand: isNotifications, bgRed: isHighlighted,
radiusSmall: isNotifications, circle: isHighlighted,
}) })
return ( return (

View File

@ -22,6 +22,8 @@ export const URL_GAB_PRO = 'https://pro.gab.com'
export const PLACEHOLDER_MISSING_HEADER_SRC = '/original/missing.png' export const PLACEHOLDER_MISSING_HEADER_SRC = '/original/missing.png'
export const POPOVER_CHAT_CONVERSATION_OPTIONS = 'CHAT_CONVERSATION_OPTIONS'
export const POPOVER_CHAT_MESSAGE_DELETE = 'CHAT_MESSAGE_DELETE'
export const POPOVER_COMMENT_SORTING_OPTIONS = 'COMMENT_SORTING_OPTIONS' export const POPOVER_COMMENT_SORTING_OPTIONS = 'COMMENT_SORTING_OPTIONS'
export const POPOVER_DATE_PICKER = 'DATE_PICKER' export const POPOVER_DATE_PICKER = 'DATE_PICKER'
export const POPOVER_EMOJI_PICKER = 'EMOJI_PICKER' export const POPOVER_EMOJI_PICKER = 'EMOJI_PICKER'

View File

@ -11,6 +11,7 @@ import { ScrollContext } from 'react-router-scroll-4'
import { IntlProvider, addLocaleData } from 'react-intl' import { IntlProvider, addLocaleData } from 'react-intl'
import { fetchCustomEmojis } from '../actions/custom_emojis' import { fetchCustomEmojis } from '../actions/custom_emojis'
import { fetchPromotions } from '../actions/promotions' import { fetchPromotions } from '../actions/promotions'
import { fetchChatConversationUnreadCount } from '../actions/chat_conversations'
import { hydrateStore } from '../actions/store' import { hydrateStore } from '../actions/store'
import { MIN_ACCOUNT_CREATED_AT_ONBOARDING } from '../constants' import { MIN_ACCOUNT_CREATED_AT_ONBOARDING } from '../constants'
import { import {
@ -35,6 +36,7 @@ const hydrateAction = hydrateStore(initialState)
store.dispatch(hydrateAction) store.dispatch(hydrateAction)
store.dispatch(fetchCustomEmojis()) store.dispatch(fetchCustomEmojis())
store.dispatch(fetchPromotions()) store.dispatch(fetchPromotions())
store.dispatch(fetchChatConversationUnreadCount())
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
accountCreatedAt: !!me ? state.getIn(['accounts', me, 'created_at']) : undefined, accountCreatedAt: !!me ? state.getIn(['accounts', me, 'created_at']) : undefined,

View File

@ -17,7 +17,6 @@ import ScrollableList from '../../../components/scrollable_list'
class ChatConversationsList extends ImmutablePureComponent { class ChatConversationsList extends ImmutablePureComponent {
componentDidMount() { componentDidMount() {
console.log("componentDidMount:", this.props.source)
this.props.onFetchChatConversations(this.props.source) this.props.onFetchChatConversations(this.props.source)
} }
@ -33,8 +32,6 @@ class ChatConversationsList extends ImmutablePureComponent {
chatConversationIds, chatConversationIds,
} = this.props } = this.props
console.log("---source:", source, chatConversationIds)
return ( return (
<div className={[_s.d, _s.w100PC, _s.overflowHidden, _s.boxShadowNone].join(' ')}> <div className={[_s.d, _s.w100PC, _s.overflowHidden, _s.boxShadowNone].join(' ')}>
<ScrollableList <ScrollableList

View File

@ -61,7 +61,9 @@ class ChatConversationsListItem extends ImmutablePureComponent {
const avatarSize = 46 const avatarSize = 46
const otherAccounts = chatConversation.get('other_accounts') const otherAccounts = chatConversation.get('other_accounts')
const lastMessage = chatConversation.get('last_chat_message', null) const lastMessage = chatConversation.get('last_chat_message', null)
const content = { __html: !!lastMessage ? lastMessage.get('text', '') : '' } let lastMessageText = !!lastMessage ? lastMessage.get('text', '') : ''
lastMessageText = lastMessageText.length >= 28 ? `${lastMessageText.substring(0, 28).trim()}...` : lastMessageText
const content = { __html: lastMessageText }
const date = !!lastMessage ? lastMessage.get('created_at') : chatConversation.get('created_at') const date = !!lastMessage ? lastMessage.get('created_at') : chatConversation.get('created_at')
return ( return (

View File

@ -9,6 +9,7 @@ import { sendChatMessage } from '../../../actions/chat_messages'
import { CX } from '../../../constants' import { CX } from '../../../constants'
import Button from '../../../components/button' import Button from '../../../components/button'
import Input from '../../../components/input' import Input from '../../../components/input'
import Text from '../../../components/text'
class ChatMessagesComposeForm extends React.PureComponent { class ChatMessagesComposeForm extends React.PureComponent {
@ -90,13 +91,13 @@ class ChatMessagesComposeForm extends React.PureComponent {
maxH200PX: 1, maxH200PX: 1,
borderColorSecondary: 1, borderColorSecondary: 1,
border1PX: 1, border1PX: 1,
radiusSmall: 1, radiusRounded: 1,
py10: 1, py10: 1,
}) })
return ( return (
<div className={[_s.d, _s.posAbs, _s.bottom0, _s.left0, _s.right0, _s.flexRow, _s.aiCenter, _s.minH58PX, _s.bgPrimary, _s.w100PC, _s.borderTop1PX, _s.borderColorSecondary, _s.px15, _s.py5].join(' ')}> <div className={[_s.d, _s.posAbs, _s.bottom0, _s.left0, _s.right0, _s.flexRow, _s.aiCenter, _s.minH58PX, _s.bgPrimary, _s.w100PC, _s.borderTop1PX, _s.borderColorSecondary, _s.px15].join(' ')}>
<div className={[_s.d, _s.pr15, _s.flexGrow1].join(' ')}> <div className={[_s.d, _s.pr15, _s.flexGrow1, _s.py10].join(' ')}>
<Textarea <Textarea
id='chat-message-compose-input' id='chat-message-compose-input'
inputRef={this.setTextbox} inputRef={this.setTextbox}
@ -112,12 +113,12 @@ class ChatMessagesComposeForm extends React.PureComponent {
aria-autocomplete='list' aria-autocomplete='list'
/> />
</div> </div>
<div className={[_s.d, _s.h100PC, _s.mtAuto, _s.pb5].join(' ')}> <div className={[_s.d, _s.h100PC, _s.aiCenter, _s.jcCenter].join(' ')}>
<Button <Button
disabled={disabled} disabled={disabled}
onClick={this.handleOnSendChatMessage} onClick={this.handleOnSendChatMessage}
> >
Send <Text color='inherit' className={_s.px10}>Send</Text>
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -4,9 +4,11 @@ import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes' import ImmutablePropTypes from 'react-immutable-proptypes'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { makeGetChatConversation } from '../../../selectors' import { makeGetChatConversation } from '../../../selectors'
import { openModal } from '../../../actions/modal' import { openPopover } from '../../../actions/popover'
import { approveChatConversationRequest } from '../../../actions/chat_conversations' import { approveChatConversationRequest } from '../../../actions/chat_conversations'
import { MODAL_CHAT_CONVERSATION_CREATE } from '../../../constants' import {
POPOVER_CHAT_CONVERSATION_OPTIONS
} from '../../../constants'
import Button from '../../../components/button' import Button from '../../../components/button'
import AvatarGroup from '../../../components/avatar_group' import AvatarGroup from '../../../components/avatar_group'
import DisplayName from '../../../components/display_name' import DisplayName from '../../../components/display_name'
@ -18,6 +20,14 @@ class ChatMessageHeader extends React.PureComponent {
this.props.onApproveChatConversationRequest(this.props.chatConversationId) this.props.onApproveChatConversationRequest(this.props.chatConversationId)
} }
handleOnOpenChatConversationOptionsPopover = () => {
this.props.onOpenChatConversationOptionsPopover(this.props.chatConversationId, this.optionsBtnRef)
}
setOptionsBtnRef = (c) => {
this.optionsBtnRef = c
}
render () { render () {
const { chatConversation } = this.props const { chatConversation } = this.props
@ -37,8 +47,9 @@ class ChatMessageHeader extends React.PureComponent {
</React.Fragment> </React.Fragment>
} }
<Button <Button
buttonRef={this.setOptionsBtnRef}
isNarrow isNarrow
onClick={undefined} onClick={this.handleOnOpenChatConversationOptionsPopover}
color='primary' color='primary'
backgroundColor='secondary' backgroundColor='secondary'
className={[_s.mlAuto, _s.px5].join(' ')} className={[_s.mlAuto, _s.px5].join(' ')}
@ -68,17 +79,22 @@ const mapStateToProps = (state, { chatConversationId }) => ({
}) })
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
onOpenChatConversationCreateModal() {
dispatch(openModal(MODAL_CHAT_CONVERSATION_CREATE))
},
onApproveChatConversationRequest(chatConversationId) { onApproveChatConversationRequest(chatConversationId) {
dispatch(approveChatConversationRequest(chatConversationId)) dispatch(approveChatConversationRequest(chatConversationId))
} },
onOpenChatConversationOptionsPopover(chatConversationId, targetRef) {
dispatch(openPopover(POPOVER_CHAT_CONVERSATION_OPTIONS, {
chatConversationId,
targetRef,
position: 'bottom',
}))
},
}) })
ChatMessageHeader.propTypes = { ChatMessageHeader.propTypes = {
onOpenChatConversationCreateModal: PropTypes.func.isRequired,
chatConversationId: PropTypes.string, chatConversationId: PropTypes.string,
onApproveChatConversationRequest: PropTypes.func.isRequired,
onOpenChatConversationOptionsPopover: PropTypes.func.isRequired,
} }
export default connect(mapStateToProps, mapDispatchToProps)(ChatMessageHeader) export default connect(mapStateToProps, mapDispatchToProps)(ChatMessageHeader)

View File

@ -5,7 +5,11 @@ import moment from 'moment-mini'
import ImmutablePureComponent from 'react-immutable-pure-component' import ImmutablePureComponent from 'react-immutable-pure-component'
import ImmutablePropTypes from 'react-immutable-proptypes' import ImmutablePropTypes from 'react-immutable-proptypes'
import { NavLink } from 'react-router-dom' import { NavLink } from 'react-router-dom'
import { CX } from '../../../constants' import { openPopover } from '../../../actions/popover'
import {
CX,
POPOVER_CHAT_MESSAGE_DELETE,
} from '../../../constants'
import { me } from '../../../initial_state' import { me } from '../../../initial_state'
import Input from '../../../components/input' import Input from '../../../components/input'
import Avatar from '../../../components/avatar' import Avatar from '../../../components/avatar'
@ -47,7 +51,11 @@ class ChatMessageItem extends ImmutablePureComponent {
} }
handleMoreClick = () => { handleMoreClick = () => {
// this.props.onOpenChatMessageDeletePopover(this.props.chatMessageId, this.deleteBtnRef)
}
setDeleteBtnRef = (c) => {
this.deleteBtnRef = c
} }
render() { render() {
@ -65,6 +73,8 @@ class ChatMessageItem extends ImmutablePureComponent {
if (!chatMessage) return <div /> if (!chatMessage) return <div />
const account = chatMessage.get('account') const account = chatMessage.get('account')
if (!account) return <div />
const content = { __html: chatMessage.get('text') } const content = { __html: chatMessage.get('text') }
const alt = account.get('id', null) === me const alt = account.get('id', null) === me
const createdAt = chatMessage.get('created_at') const createdAt = chatMessage.get('created_at')
@ -89,9 +99,10 @@ class ChatMessageItem extends ImmutablePureComponent {
d: 1, d: 1,
px15: 1, px15: 1,
py5: 1, py5: 1,
bgTertiary: !alt, maxW80PC: 1,
bgSecondary: alt, bgTertiary: alt,
circle: 1, bgSecondary: !alt,
radiusRounded: 1,
ml10: 1, ml10: 1,
mr10: 1, mr10: 1,
}) })
@ -138,6 +149,7 @@ class ChatMessageItem extends ImmutablePureComponent {
alt && alt &&
<div className={buttonContainerClasses}> <div className={buttonContainerClasses}>
<Button <Button
buttonRef={this.setDeleteBtnRef}
onClick={this.handleMoreClick} onClick={this.handleMoreClick}
color='tertiary' color='tertiary'
backgroundColor='none' backgroundColor='none'
@ -165,15 +177,25 @@ const mapStateToProps = (state, { lastChatMessageId, chatMessageId }) => ({
lastChatMessageSameSender: lastChatMessageId ? state.getIn(['chat_messages', `${lastChatMessageId}`, 'from_account_id'], null) === state.getIn(['chat_messages', `${chatMessageId}`, 'from_account_id'], null) : false, lastChatMessageSameSender: lastChatMessageId ? state.getIn(['chat_messages', `${lastChatMessageId}`, 'from_account_id'], null) === state.getIn(['chat_messages', `${chatMessageId}`, 'from_account_id'], null) : false,
}) })
const mapDispatchToProps = (dispatch) => ({
onOpenChatMessageDeletePopover(chatMessageId, targetRef) {
dispatch(openPopover(POPOVER_CHAT_MESSAGE_DELETE, {
targetRef,
chatMessageId,
position: 'top',
}))
},
})
ChatMessageItem.propTypes = { ChatMessageItem.propTypes = {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
lastChatMessageId: PropTypes.string, lastChatMessageId: PropTypes.string,
lastChatMessageDate: PropTypes.string, lastChatMessageDate: PropTypes.string,
lastChatMessageSameSender: PropTypes.string, lastChatMessageSameSender: PropTypes.bool,
chatMessageId: PropTypes.string.isRequired, chatMessageId: PropTypes.string.isRequired,
chatMessage: ImmutablePropTypes.map, chatMessage: ImmutablePropTypes.map,
isHidden: PropTypes.bool, isHidden: PropTypes.bool,
alt: PropTypes.bool, alt: PropTypes.bool,
} }
export default connect(mapStateToProps)(ChatMessageItem) export default connect(mapStateToProps, mapDispatchToProps)(ChatMessageItem)

View File

@ -208,7 +208,7 @@ class SwitchingArea extends React.PureComponent {
<WrappedRoute path='/messages/requests' exact page={MessagesPage} component={ChatConversationRequests} content={children} componentParams={{ isSettings: true, source: 'requested' }} /> <WrappedRoute path='/messages/requests' exact page={MessagesPage} component={ChatConversationRequests} content={children} componentParams={{ isSettings: true, source: 'requested' }} />
<WrappedRoute path='/messages/blocks' exact page={MessagesPage} component={ChatConversationBlockedAccounts} content={children} componentParams={{ isSettings: true }} /> <WrappedRoute path='/messages/blocks' exact page={MessagesPage} component={ChatConversationBlockedAccounts} content={children} componentParams={{ isSettings: true }} />
<WrappedRoute path='/messages/mutes' exact page={MessagesPage} component={ChatConversationMutedAccounts} content={children} componentParams={{ isSettings: true }} /> <WrappedRoute path='/messages/mutes' exact page={MessagesPage} component={ChatConversationMutedAccounts} content={children} componentParams={{ isSettings: true }} />
<WrappedRoute path='/messages/:conversationId' exact page={MessagesPage} component={Messages} content={children} componentParams={{ source: 'approved' }} /> <WrappedRoute path='/messages/:chatConversationId' exact page={MessagesPage} component={Messages} content={children} componentParams={{ source: 'approved' }} />
<WrappedRoute path='/timeline/all' exact page={CommunityPage} component={CommunityTimeline} content={children} componentParams={{ title: 'Community Feed' }} /> <WrappedRoute path='/timeline/all' exact page={CommunityPage} component={CommunityTimeline} content={children} componentParams={{ title: 'Community Feed' }} />
<WrappedRoute path='/timeline/pro' exact page={ProPage} component={ProTimeline} content={children} componentParams={{ title: 'Pro Feed' }} /> <WrappedRoute path='/timeline/pro' exact page={ProPage} component={ProTimeline} content={children} componentParams={{ title: 'Pro Feed' }} />

View File

@ -12,7 +12,9 @@ export function ChatConversationCreate() { return import(/* webpackChunkName: "f
export function ChatConversationCreateModal() { return import(/* webpackChunkName: "components/chat_conversation_create_modal" */'../../../components/modal/chat_conversation_create_modal') } export function ChatConversationCreateModal() { return import(/* webpackChunkName: "components/chat_conversation_create_modal" */'../../../components/modal/chat_conversation_create_modal') }
export function ChatConversationDeleteModal() { return import(/* webpackChunkName: "components/chat_conversation_delete_modal" */'../../../components/modal/chat_conversation_delete_modal') } export function ChatConversationDeleteModal() { return import(/* webpackChunkName: "components/chat_conversation_delete_modal" */'../../../components/modal/chat_conversation_delete_modal') }
export function ChatConversationMutedAccounts() { return import(/* webpackChunkName: "features/chat_conversation_muted_accounts" */'../../chat_conversation_muted_accounts') } export function ChatConversationMutedAccounts() { return import(/* webpackChunkName: "features/chat_conversation_muted_accounts" */'../../chat_conversation_muted_accounts') }
export function ChatConversationOptionsPopover() { return import(/* webpackChunkName: "components/chat_conversation_options_popover" */'../../../components/popover/chat_conversation_options_popover') }
export function ChatConversationRequests() { return import(/* webpackChunkName: "features/chat_conversation_requests" */'../../chat_conversation_requests') } export function ChatConversationRequests() { return import(/* webpackChunkName: "features/chat_conversation_requests" */'../../chat_conversation_requests') }
export function ChatMessageDeletePopover() { return import(/* webpackChunkName: "components/chat_message_delete_popover" */'../../../components/popover/chat_message_delete_popover') }
export function CommentSortingOptionsPopover() { return import(/* webpackChunkName: "components/comment_sorting_options_popover" */'../../../components/popover/comment_sorting_options_popover') } export function CommentSortingOptionsPopover() { return import(/* webpackChunkName: "components/comment_sorting_options_popover" */'../../../components/popover/comment_sorting_options_popover') }
export function CommunityTimeline() { return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline') } export function CommunityTimeline() { return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline') }
export function CommunityTimelineSettingsModal() { return import(/* webpackChunkName: "components/community_timeline_settings_modal" */'../../../components/modal/community_timeline_settings_modal') } export function CommunityTimelineSettingsModal() { return import(/* webpackChunkName: "components/community_timeline_settings_modal" */'../../../components/modal/community_timeline_settings_modal') }

View File

@ -41,8 +41,6 @@ class MessagesLayout extends React.PureComponent {
jcEnd: 1, jcEnd: 1,
}) })
console.log("currentConversationIsRequest:",currentConversationIsRequest)
return ( return (
<Layout <Layout
showBackBtn showBackBtn

View File

@ -70,6 +70,7 @@ class HomePage extends React.PureComponent {
intl, intl,
isPro, isPro,
totalQueuedItemsCount, totalQueuedItemsCount,
unreadChatsCount,
} = this.props } = this.props
const { lazyLoaded } = this.state const { lazyLoaded } = this.state
@ -80,6 +81,11 @@ class HomePage extends React.PureComponent {
page='home' page='home'
title={title} title={title}
actions={[ actions={[
{
icon: 'chat',
to: '/messages',
count: unreadChatsCount,
},
{ {
icon: 'search', icon: 'search',
to: '/search', to: '/search',
@ -117,6 +123,7 @@ const messages = defineMessages({
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
totalQueuedItemsCount: state.getIn(['timelines', 'home', 'totalQueuedItemsCount']), totalQueuedItemsCount: state.getIn(['timelines', 'home', 'totalQueuedItemsCount']),
unreadChatsCount: state.getIn(['chats', 'chatsUnreadCount']),
isPro: state.getIn(['accounts', me, 'is_pro']), isPro: state.getIn(['accounts', me, 'is_pro']),
}) })
@ -125,6 +132,7 @@ HomePage.propTypes = {
dispatch: PropTypes.func.isRequired, dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
isPro: PropTypes.bool, isPro: PropTypes.bool,
unreadChatsCount: PropTypes.number.isRequired,
totalQueuedItemsCount: PropTypes.number.isRequired, totalQueuedItemsCount: PropTypes.number.isRequired,
} }

View File

@ -1,10 +1,23 @@
import React from 'react' import React from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import isObject from 'lodash.isobject'
import { setChatConversationSelected } from '../actions/chats'
import PageTitle from '../features/ui/util/page_title' import PageTitle from '../features/ui/util/page_title'
import MessagesLayout from '../layouts/messages_layout' import MessagesLayout from '../layouts/messages_layout'
class MessagesPage extends React.PureComponent { class MessagesPage extends React.PureComponent {
componentDidMount() {
if (isObject(this.props.params)) {
const { chatConversationId } = this.props.params
if (chatConversationId) {
this.props.dispatch(setChatConversationSelected(chatConversationId))
}
}
}
render() { render() {
const { const {
children, children,
@ -34,4 +47,4 @@ MessagesPage.propTypes = {
source: PropTypes.string, source: PropTypes.string,
} }
export default MessagesPage export default connect()(MessagesPage)

View File

@ -97,8 +97,10 @@ export default function chat_conversation_messages(state = initialState, action)
case CHAT_CONVERSATION_MESSAGES_EXPAND_SUCCESS: case CHAT_CONVERSATION_MESSAGES_EXPAND_SUCCESS:
return expandNormalizedChatConversation(state, action.chatConversationId, fromJS(action.chatMessages), action.next, action.partial, action.isLoadingRecent) return expandNormalizedChatConversation(state, action.chatConversationId, fromJS(action.chatMessages), action.next, action.partial, action.isLoadingRecent)
case CHAT_MESSAGES_SEND_SUCCESS: case CHAT_MESSAGES_SEND_SUCCESS:
return updateChatMessageConversation(state, action.chatConversationId, fromJS(action.chatMessage)) return updateChatMessageConversation(state, action.chatMessage.chat_conversation_id, fromJS(action.chatMessage))
// CHAT_MESSAGES_DELETE_REQUEST case CHAT_MESSAGES_DELETE_REQUEST:
// : todo :
return state
default: default:
return state return state
} }

View File

@ -4,6 +4,10 @@ import {
fromJS, fromJS,
} from 'immutable' } from 'immutable'
import { me } from '../initial_state' import { me } from '../initial_state'
import {
CHAT_MESSAGES_SEND_SUCCESS,
CHAT_MESSAGES_DELETE_REQUEST,
} from '../actions/chat_messages'
import { import {
CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS, CHAT_CONVERSATIONS_APPROVED_FETCH_SUCCESS,
CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS, CHAT_CONVERSATIONS_APPROVED_EXPAND_SUCCESS,
@ -22,6 +26,10 @@ export const normalizeChatConversation = (chatConversation) => {
}) })
} }
const setLastChatMessage = (state, chatMessage) => {
return state.setIn([chatMessage.chat_conversation_id, 'last_chat_message'], fromJS(chatMessage))
}
const importChatConversation = (state, chatConversation) => state.set(chatConversation.chat_conversation_id, normalizeChatConversation(chatConversation)) const importChatConversation = (state, chatConversation) => state.set(chatConversation.chat_conversation_id, normalizeChatConversation(chatConversation))
const importChatConversations = (state, chatConversations) => { const importChatConversations = (state, chatConversations) => {
@ -37,6 +45,11 @@ export default function chat_conversations(state = initialState, action) {
case CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS: case CHAT_CONVERSATIONS_REQUESTED_FETCH_SUCCESS:
case CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS: case CHAT_CONVERSATIONS_REQUESTED_EXPAND_SUCCESS:
return importChatConversations(state, action.chatConversations) return importChatConversations(state, action.chatConversations)
case CHAT_MESSAGES_SEND_SUCCESS:
return setLastChatMessage(state, action.chatMessage)
case CHAT_MESSAGES_DELETE_REQUEST:
// : todo : set last conversation message to one prior to this one
return state
default: default:
return state return state
} }

View File

@ -9,11 +9,10 @@ import {
SET_CHAT_CONVERSATION_SELECTED, SET_CHAT_CONVERSATION_SELECTED,
} from '../actions/chats' } from '../actions/chats'
import { import {
CHAT_CONVERSATION_APPROVED_UNREAD_COUNT_FETCH_SUCCESS,
CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS, CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS,
} from '../actions/chat_conversations' } from '../actions/chat_conversations'
import { import {
CHAT_MESSAGES_SEND_SUCCESS,
CHAT_MESSAGES_DELETE_REQUEST,
CHAT_MESSAGES_FETCH_SUCCESS, CHAT_MESSAGES_FETCH_SUCCESS,
CHAT_CONVERSATION_MESSAGES_EXPAND_SUCCESS, CHAT_CONVERSATION_MESSAGES_EXPAND_SUCCESS,
} from '../actions/chat_messages' } from '../actions/chat_messages'
@ -22,6 +21,7 @@ const initialState = ImmutableMap({
createChatConversationSuggestionIds: ImmutableList(), createChatConversationSuggestionIds: ImmutableList(),
selectedChatConversationId: null, selectedChatConversationId: null,
chatConversationRequestCount: 0, chatConversationRequestCount: 0,
chatsUnreadCount: 0,
}) })
export default function chats(state = initialState, action) { export default function chats(state = initialState, action) {
@ -32,6 +32,8 @@ export default function chats(state = initialState, action) {
return state.set('selectedChatConversationId', action.chatConversationId) return state.set('selectedChatConversationId', action.chatConversationId)
case CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS: case CHAT_CONVERSATION_REQUESTED_COUNT_FETCH_SUCCESS:
return state.set('chatConversationRequestCount', action.count) return state.set('chatConversationRequestCount', action.count)
case CHAT_CONVERSATION_APPROVED_UNREAD_COUNT_FETCH_SUCCESS:
return state.set('chatsUnreadCount', action.count)
default: default:
return state return state
} }

View File

@ -43,7 +43,6 @@ export function connectStream(path, pollingRefresh = null, callbacks = () => ({
}, },
received(data) { received(data) {
console.log("received:", data)
onReceive(data); onReceive(data);
}, },

View File

@ -15,6 +15,7 @@
--color_red-dark: #c72c5b; --color_red-dark: #c72c5b;
--radius-small: 8px; --radius-small: 8px;
--radius-rounded: 20px;
--radius-circle: 9999px; --radius-circle: 9999px;
--fs_xs: 0.8571428571rem; --fs_xs: 0.8571428571rem;
@ -73,9 +74,9 @@
:root[no-radius] { :root[no-radius] {
--radius-small: 0 !important; --radius-small: 0 !important;
--radius-rounded: 0 !important;
} }
:root[theme='white'] { :root[theme='white'] {
--navigation_background: var(--color_white); --navigation_background: var(--color_white);
--navigation_blend: #aaa; --navigation_blend: #aaa;
@ -357,6 +358,7 @@ pre {
.circle { border-radius: var(--radius-circle); } .circle { border-radius: var(--radius-circle); }
.radiusSmall { border-radius: var(--radius-small); } .radiusSmall { border-radius: var(--radius-small); }
.radiusRounded { border-radius: var(--radius-rounded); }
.topLeftRadiusSmall { border-top-left-radius: var(--radius-small); } .topLeftRadiusSmall { border-top-left-radius: var(--radius-small); }
.topRightRadiusSmall { border-top-right-radius: var(--radius-small); } .topRightRadiusSmall { border-top-right-radius: var(--radius-small); }
.bottomRightRadiusSmall { border-bottom-right-radius: var(--radius-small); } .bottomRightRadiusSmall { border-bottom-right-radius: var(--radius-small); }
@ -502,6 +504,7 @@ pre {
.left50PC { left: 50%; } .left50PC { left: 50%; }
.right0 { right: 0px; } .right0 { right: 0px; }
.rightNeg5PX { right: -5px; }
.rightAuto { right: auto; } .rightAuto { right: auto; }
/* */ /* */

View File

@ -15,6 +15,8 @@ class InlineRenderer
serializer = REST::NotificationSerializer serializer = REST::NotificationSerializer
when :conversation when :conversation
serializer = REST::ConversationSerializer serializer = REST::ConversationSerializer
when :chat_message
serializer = REST::ChatMessageSerializer
else else
return return
end end

View File

@ -14,8 +14,6 @@
# #
class AccountConversation < ApplicationRecord class AccountConversation < ApplicationRecord
after_commit :push_to_streaming_api
belongs_to :account belongs_to :account
belongs_to :conversation belongs_to :conversation
belongs_to :last_status, class_name: 'Status' belongs_to :last_status, class_name: 'Status'
@ -102,11 +100,6 @@ class AccountConversation < ApplicationRecord
self.last_status_id = status_ids.last self.last_status_id = status_ids.last
end end
def push_to_streaming_api
return if destroyed? || !subscribed_to_timeline?
PushConversationWorker.perform_async(id)
end
def subscribed_to_timeline? def subscribed_to_timeline?
Redis.current.exists("subscribed:#{streaming_channel}") Redis.current.exists("subscribed:#{streaming_channel}")
end end

View File

@ -4,10 +4,10 @@
# Table name: chat_blocks # Table name: chat_blocks
# #
# id :bigint(8) not null, primary key # id :bigint(8) not null, primary key
# account_id :integer not null
# target_account_id :integer not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# account_id :bigint(8) not null
# target_account_id :bigint(8) not null
# #
class ChatBlock < ApplicationRecord class ChatBlock < ApplicationRecord

View File

@ -8,11 +8,12 @@
# chat_conversation_id :bigint(8) # chat_conversation_id :bigint(8)
# participant_account_ids :bigint(8) default([]), not null, is an Array # participant_account_ids :bigint(8) default([]), not null, is an Array
# last_chat_message_id :bigint(8) # last_chat_message_id :bigint(8)
# is_unread :boolean default(FALSE), not null
# is_hidden :boolean default(FALSE), not null # is_hidden :boolean default(FALSE), not null
# is_approved :boolean default(FALSE), not null # is_approved :boolean default(FALSE), not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# unread_count :bigint(8) default(0), not null
# chat_message_expiration_policy :string
# #
class ChatConversationAccount < ApplicationRecord class ChatConversationAccount < ApplicationRecord
@ -28,7 +29,6 @@ class ChatConversationAccount < ApplicationRecord
if participant_account_ids.empty? if participant_account_ids.empty?
[account] [account]
else else
# : todo : dont include current_account
participants = Account.where(id: participant_account_ids) participants = Account.where(id: participant_account_ids)
participants.empty? ? [account] : participants participants.empty? ? [account] : participants
end end

View File

@ -4,12 +4,13 @@
# Table name: chat_messages # Table name: chat_messages
# #
# id :bigint(8) not null, primary key # id :bigint(8) not null, primary key
# account_id :bigint(8) not null
# chat_conversation_id :bigint(8) not null
# text :text default(""), not null # text :text default(""), not null
# language :string # language :text default(""), not null
# from_account_id :integer not null
# chat_conversation_id :integer not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# expires_at :datetime
# #
class ChatMessage < ApplicationRecord class ChatMessage < ApplicationRecord

View File

@ -4,10 +4,10 @@
# Table name: chat_mutes # Table name: chat_mutes
# #
# id :bigint(8) not null, primary key # id :bigint(8) not null, primary key
# account_id :integer not null
# target_account_id :integer not null
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# account_id :bigint(8) not null
# target_account_id :bigint(8) not null
# #
class ChatMute < ApplicationRecord class ChatMute < ApplicationRecord

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class REST::ChatConversationAccountSerializer < ActiveModel::Serializer class REST::ChatConversationAccountSerializer < ActiveModel::Serializer
attributes :id, :is_hidden, :is_approved, :is_unread, :chat_conversation_id, :created_at attributes :id, :is_hidden, :is_approved, :unread_count, :is_unread, :chat_conversation_id, :created_at
has_many :participant_accounts, key: :other_accounts, serializer: REST::AccountSerializer has_many :participant_accounts, key: :other_accounts, serializer: REST::AccountSerializer
has_one :last_chat_message, serializer: REST::ChatMessageSerializer, unless: :last_chat_message_id? has_one :last_chat_message, serializer: REST::ChatMessageSerializer, unless: :last_chat_message_id?
@ -18,4 +18,8 @@ class REST::ChatConversationAccountSerializer < ActiveModel::Serializer
object.last_chat_message_id.nil? object.last_chat_message_id.nil?
end end
def is_unread
object.unread_count > 0
end
end end

View File

@ -7,4 +7,13 @@ class REST::ChatMessageSerializer < ActiveModel::Serializer
def id def id
object.id.to_s object.id.to_s
end end
def from_account_id
object.from_account_id.to_s
end
def chat_conversation_id
object.chat_conversation_id.to_s
end
end end

View File

@ -2,11 +2,6 @@
= t('admin.accounts.title') = t('admin.accounts.title')
.filters .filters
.filter-subset
%strong= t('admin.accounts.location.title')
%ul
%li= filter_link_to t('admin.accounts.location.local'), remote: nil
%li= filter_link_to t('admin.accounts.location.remote'), remote: '1'
.filter-subset .filter-subset
%strong= t('admin.accounts.moderation.title') %strong= t('admin.accounts.moderation.title')
%ul %ul

View File

@ -1,29 +1,13 @@
- content_for :page_title do - content_for :page_title do
= t('admin.custom_emojis.title') = t('admin.custom_emojis.title')
.filters
.filter-subset
%strong= t('admin.accounts.location.title')
%ul
%li= filter_link_to t('admin.accounts.location.all'), local: nil, remote: nil
%li
- if selected? local: '1', remote: nil
= filter_link_to t('admin.accounts.location.local'), {local: nil, remote: nil}, {local: '1', remote: nil}
- else
= filter_link_to t('admin.accounts.location.local'), local: '1', remote: nil
%li
- if selected? remote: '1', local: nil
= filter_link_to t('admin.accounts.location.remote'), {remote: nil, local: nil}, {remote: '1', local: nil}
- else
= filter_link_to t('admin.accounts.location.remote'), remote: '1', local: nil
= form_tag admin_custom_emojis_url, method: 'GET', class: 'simple_form' do = form_tag admin_custom_emojis_url, method: 'GET', class: 'simple_form' do
.fields-group .fields-group
- Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.each do |key| - Admin::FilterHelper::CUSTOM_EMOJI_FILTERS.each do |key|
- if params[key].present? - if params[key].present?
= hidden_field_tag key, params[key] = hidden_field_tag key, params[key]
- %i(shortcode by_domain).each do |key| - %i(shortcode).each do |key|
.input.string.optional .input.string.optional
= text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.custom_emojis.#{key}") = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.custom_emojis.#{key}")

View File

@ -234,8 +234,16 @@ Rails.application.routes.draw do
end end
namespace :chat_conversations do namespace :chat_conversations do
resources :messages, only: :show resources :messages, only: :show do
resources :approved_conversations member do
delete :destroy_all
end
end
resources :approved_conversations, only: :index do
collection do
get :unread_count
end
end
resources :requested_conversations, only: :index do resources :requested_conversations, only: :index do
collection do collection do
get :count get :count

View File

@ -18,9 +18,6 @@
scheduled_statuses_scheduler: scheduled_statuses_scheduler:
every: '1m' every: '1m'
class: Scheduler::ScheduledStatusesScheduler class: Scheduler::ScheduledStatusesScheduler
subscriptions_scheduler:
cron: '<%= Random.rand(0..59) %> <%= Random.rand(4..6) %> * * *'
class: Scheduler::SubscriptionsScheduler
media_cleanup_scheduler: media_cleanup_scheduler:
cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *' cron: '<%= Random.rand(0..59) %> <%= Random.rand(3..5) %> * * *'
class: Scheduler::MediaCleanupScheduler class: Scheduler::MediaCleanupScheduler

View File

@ -0,0 +1,10 @@
class AddUnreadCountToChatConversationAccounts < ActiveRecord::Migration[5.2]
def up
add_column :chat_conversation_accounts, :unread_count, :bigint
change_column_default :chat_conversation_accounts, :unread_count, 0
end
def down
remove_column :chat_conversation_accounts, :unread_count
end
end

View File

@ -0,0 +1,5 @@
class RemoveIsUnreadFromChatConversationAccounts < ActiveRecord::Migration[5.2]
def change
safety_assured { remove_column :chat_conversation_accounts, :is_unread }
end
end

View File

@ -0,0 +1,5 @@
class AddExpiresAtToChatMessages < ActiveRecord::Migration[5.2]
def change
add_column :chat_messages, :expires_at, :datetime, null: true, default: nil
end
end

View File

@ -0,0 +1,10 @@
class BackfillAddUnreadCountToChatConversationAccounts < ActiveRecord::Migration[5.2]
disable_ddl_transaction!
def change
ChatConversationAccount.in_batches do |relation|
relation.update_all unread_count: 0
sleep(0.1)
end
end
end

View File

@ -0,0 +1,5 @@
class AddUnreadCountToChatConversationAccountsNotNull < ActiveRecord::Migration[5.2]
def change
change_column_null :chat_conversation_accounts, :unread_count, false
end
end

View File

@ -0,0 +1,5 @@
class AddChatMessageExpirationPolicyToChatConversationAccounts < ActiveRecord::Migration[5.2]
def change
add_column :chat_conversation_accounts, :chat_message_expiration_policy, :string, null: true, default: nil
end
end

View File

@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_11_30_172733) do ActiveRecord::Schema.define(version: 2020_12_03_214600) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements" enable_extension "pg_stat_statements"
@ -207,11 +207,12 @@ ActiveRecord::Schema.define(version: 2020_11_30_172733) do
t.bigint "chat_conversation_id" t.bigint "chat_conversation_id"
t.bigint "participant_account_ids", default: [], null: false, array: true t.bigint "participant_account_ids", default: [], null: false, array: true
t.bigint "last_chat_message_id" t.bigint "last_chat_message_id"
t.boolean "is_unread", default: false, null: false
t.boolean "is_hidden", default: false, null: false t.boolean "is_hidden", default: false, null: false
t.boolean "is_approved", default: false, null: false t.boolean "is_approved", default: false, null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.bigint "unread_count", default: 0, null: false
t.string "chat_message_expiration_policy"
t.index ["account_id"], name: "index_chat_conversation_accounts_on_account_id" t.index ["account_id"], name: "index_chat_conversation_accounts_on_account_id"
t.index ["chat_conversation_id"], name: "index_chat_conversation_accounts_on_chat_conversation_id" t.index ["chat_conversation_id"], name: "index_chat_conversation_accounts_on_chat_conversation_id"
end end
@ -228,6 +229,7 @@ ActiveRecord::Schema.define(version: 2020_11_30_172733) do
t.integer "chat_conversation_id", null: false t.integer "chat_conversation_id", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.datetime "expires_at"
t.index ["from_account_id", "chat_conversation_id"], name: "index_chat_messages_on_from_account_id_and_chat_conversation_id" t.index ["from_account_id", "chat_conversation_id"], name: "index_chat_messages_on_from_account_id_and_chat_conversation_id"
end end

View File

@ -537,8 +537,7 @@ const startWorker = (workerId) => {
}); });
app.get('/api/v1/streaming/chat_messages', (req, res) => { app.get('/api/v1/streaming/chat_messages', (req, res) => {
console.log("tilly hello from inside here:", req) const channel = `chat_messages:1`;
const channel = `chat_messages:${req.chatConversationId}`;
streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel))); streamFrom(channel, req, streamToHttp(req, res), streamHttpEnd(req, subscriptionHeartbeat(channel)));
}); });
@ -551,8 +550,6 @@ const startWorker = (workerId) => {
let channel; let channel;
console.log("tilly location.query.stream:", location.query.stream)
switch (location.query.stream) { switch (location.query.stream) {
case 'statuscard': case 'statuscard':
channel = `statuscard:${req.accountId}`; channel = `statuscard:${req.accountId}`;
@ -566,8 +563,7 @@ const startWorker = (workerId) => {
streamFrom(`timeline:${req.accountId}`, req, streamToWs(req, ws), streamWsEnd(req, ws), false, true); streamFrom(`timeline:${req.accountId}`, req, streamToWs(req, ws), streamWsEnd(req, ws), false, true);
break; break;
case 'chat_messages': case 'chat_messages':
console.log("tilly incoming chat_messages:", req.chatConversationId, location.query.stream) streamFrom(`chat_messages:${req.accountId}`, req, streamToWs(req, ws), streamWsEnd(req, ws), false, true);
streamFrom(`chat_messages:${req.chatConversationId}`, req, streamToWs(req, ws), streamWsEnd(req, ws), false, true);
break; break;
default: default:
ws.close(); ws.close();