Clients/Client2018/content/internal/Chat/Modules/LuaChat/Components/Conversation.lua

520 lines
16 KiB
Lua

local CoreGui = game:GetService("CoreGui")
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService")
local Modules = CoreGui.RobloxGui.Modules
local Common = Modules.Common
local LuaChat = Modules.LuaChat
local LuaApp = Modules.LuaApp
local Constants = require(LuaChat.Constants)
local Conversation = require(LuaChat.Models.Conversation)
local ConversationActions = require(LuaChat.Actions.ConversationActions)
local Create = require(LuaChat.Create)
local DialogInfo = require(LuaChat.DialogInfo)
local FlagSettings = require(LuaChat.FlagSettings)
local FormFactor = require(LuaApp.Enum.FormFactor)
local Signal = require(Common.Signal)
local WebApi = require(LuaChat.WebApi)
local Components = LuaChat.Components
local PlayTogetherGameIcon = require(Components.PlayTogetherGameIcon)
local ChatInputBar = require(Components.ChatInputBar)
local ChatInputBarTablet = require(Components.ChatInputBarTablet)
local HeaderLoader = require(Components.HeaderLoader)
local LoadingIndicator = require(Components.LoadingIndicator)
local MessageList = require(Components.MessageList)
local PaddedImageButton = require(Components.PaddedImageButton)
local UserTypingIndicator = require(Components.UserTypingIndicator)
local getConversationDisplayTitle = require(LuaChat.Utils.getConversationDisplayTitle)
local Intent = DialogInfo.Intent
local LuaChatAssetCardsSelfTerminateConnection = settings():GetFFlag("LuaChatAssetCardsSelfTerminateConnection")
local FFlagLuaChatRefactoredChatInputBar = settings():GetFFlag("LuaChatRefactoredChatInputBar")
local LuaChatGroupChatIconEnabled = settings():GetFFlag("LuaChatGroupChatIconEnabled")
local FFlagShareGameToChatStatusAnalytics = settings():GetFFlag("ShareGameToChatStatusAnalytics")
local ConversationView = {}
ConversationView.__index = ConversationView
local function getNewestWithNilPreviousMessageId(messages)
for id, message, _ in messages:CreateReverseIterator() do
if message.previousMessageId == nil then
return id
end
end
return messages.keys[1]
end
local function sendPreprocess(inputText)
if inputText == "/shrug" then
return "¯\\_(ツ)_/¯"
end
-- Future chat commands will go here
return inputText
end
function ConversationView.new(appState)
local self = {}
self.connections = {}
self.conversationId = nil
self.appState = appState
self.lastTypingTimestamp = 0
self.BackButtonPressed = Signal.new()
self.GroupDetailsButtonPressed = Signal.new()
self.wasTouchingBottom = false
self.oldConversation = nil
self.luaChatPlayTogetherEnabled = FlagSettings.IsLuaChatPlayTogetherEnabled(
self.appState.store:getState().FormFactor)
setmetatable(self, ConversationView)
self.rbx = Create.new "TextButton" {
Name = "Conversation",
Text = "",
AutoButtonColor = false,
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 0,
BackgroundColor3 = Constants.Color.GRAY6,
BorderSizePixel = 0,
}
-- Component Setup
local header = HeaderLoader.GetHeader(appState, Intent.Conversation)
header:SetDefaultSubtitle()
if appState.store:getState().FormFactor == FormFactor.PHONE then
header:SetBackButtonEnabled(true)
else
header:SetBackButtonEnabled(false)
end
self.header = header
header.rbx.Parent = self.rbx
header.rbx.LayoutOrder = 1
header.rbx.ZIndex = 2 -- Render on top of the conversation (which is a peer)
local groupDetailsButton
groupDetailsButton = PaddedImageButton.new(appState, "GroupDetails",
"rbxasset://textures/ui/LuaChat/icons/ic-info.png")
header:AddButton(groupDetailsButton)
groupDetailsButton.Pressed:Connect(function()
self.GroupDetailsButtonPressed:Fire()
end)
-- Play Together feature gating:
if self.luaChatPlayTogetherEnabled then
local playTogetherGameIcon = PlayTogetherGameIcon.new(appState, nil, PlayTogetherGameIcon.Size.SMALL)
playTogetherGameIcon.Pressed:Connect(function()
self.header:ToggleGameDrawer()
self.chatInputBar.textBox:ReleaseFocus()
end)
self.playTogetherGameIcon = playTogetherGameIcon
header:AddButton(playTogetherGameIcon)
end
-- Conversation contents are now in this frame so the drawer can render
-- on top of it as necessary. Note the "HeaderSpacer" element which
-- copies the size from the real header above it.
local contents = Create.new "Frame" {
Name = "Contents",
Size = UDim2.new(1, 0, 1, 0),
BackgroundTransparency = 0,
BorderSizePixel = 0,
Create.new "UIListLayout" {
SortOrder = "LayoutOrder",
},
Create.new "Frame" {
Name = "HeaderSpacer",
Size = header.rbx.Size,
BackgroundTransparency = 0,
BorderSizePixel = 0,
},
}
contents.Parent = self.rbx
local chatInputBar
if FFlagLuaChatRefactoredChatInputBar then
chatInputBar = ChatInputBar.new(appState)
else
if appState.store:getState().FormFactor == FormFactor.PHONE then
chatInputBar = ChatInputBar.new(appState)
else
chatInputBar = ChatInputBarTablet.new(appState)
end
end
--These now get initialized in Update, based on conversationId of CurrentRoute in store
self.messageList = nil
self.messageListConnection = nil
self.typingIndicator = nil
self.initialLoadingFrame = nil
chatInputBar.rbx.Parent = contents
chatInputBar.rbx.Position = UDim2.new(0, 0, 1, -42)
chatInputBar.rbx.LayoutOrder = 4
self.chatInputBar = chatInputBar
--Close keyboard when tapping outside of both keyboard and input area
--Per spec at: https://confluence.roblox.com/display/SOCIAL/Misc+Notes
--This is a bit of a hack, but a tap that focuses self.chatInputBar.textBox
--Can also, it seems, be interpreted as a tap of self.rbx
--So if the self.chatInputBar.textBox was just focused, I won't release focus
--on tap.
local lastFocus = nil
self.chatInputBar.textBox.Focused:Connect(function()
lastFocus = tick()
self.header:InputFocus()
end)
self.rbx.TouchTap:Connect(function()
if (not lastFocus) or (tick() - lastFocus) > .3 then
self.chatInputBar.textBox:ReleaseFocus()
end
end)
-- Component Event Setup
header.BackButtonPressed:Connect(function()
self.BackButtonPressed:Fire()
end)
header.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
self:Rescale()
end)
chatInputBar.rbx:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
self:Rescale()
end)
chatInputBar.SendButtonPressed:Connect(function(text)
local messageSentLocalTime = tick()
text = sendPreprocess(text)
if FFlagShareGameToChatStatusAnalytics then
appState.store:dispatch(ConversationActions.SendMessage(self.conversationId, text, messageSentLocalTime))
else
appState.store:dispatch(ConversationActions.SendMessage(self.conversationId, text, nil, messageSentLocalTime))
end
end)
chatInputBar.UserChangedText:Connect(function()
if tick() - self.lastTypingTimestamp > Constants.Text.POST_TYPING_STATUS_INTERVAL then
self.lastTypingTimestamp = tick()
WebApi.PostTypingStatus(self.conversationId, true)
end
end)
return self
end
function ConversationView:Start()
self.header:Start()
self.header:SetConnectionState(self.appState.store:getState().ConnectionState)
if self.messageList and self.messageList.isTouchingBottom then
self.appState.store:dispatch(ConversationActions.MarkConversationAsRead(self.conversationId))
end
-- initial sizing
self:Rescale()
local propertyChangeSignal = UserInputService:GetPropertyChangedSignal("OnScreenKeyboardVisible")
local keyboardVisibleConnection = propertyChangeSignal:Connect(function()
self:TweenRescale()
end)
table.insert(self.connections, keyboardVisibleConnection)
propertyChangeSignal = UserInputService:GetPropertyChangedSignal("OnScreenKeyboardPosition")
local keyboardSizeConnection = propertyChangeSignal:Connect(function()
self:TweenRescale()
end)
table.insert(self.connections, keyboardSizeConnection)
propertyChangeSignal = self.rbx:GetPropertyChangedSignal("AbsoluteSize")
local absoluteSizeConnection = propertyChangeSignal:Connect(function()
self:TweenRescale()
end)
table.insert(self.connections, absoluteSizeConnection)
local statusBarTappedConnection = UserInputService.StatusBarTapped:Connect(function()
if self.appState.store:getState().ChatAppReducer.Location.current.intent ~= Intent.Conversation then
return
end
self.messageList.rbx:ScrollToTop()
end)
table.insert(self.connections, statusBarTappedConnection)
self:Update(self.appState.store:getState())
end
function ConversationView:Stop()
self.chatInputBar.textBox:ReleaseFocus()
if not LuaChatAssetCardsSelfTerminateConnection then
if self.messageList then
self.messageList:DisconnectChatBubbles()
end
end
for _, connection in ipairs(self.connections) do
connection:Disconnect()
end
self.connections = {}
end
function ConversationView:Pause()
self.chatInputBar.textBox:ReleaseFocus()
end
function ConversationView:Resume()
if self.messageList.isTouchingBottom then
self.appState.store:dispatch(ConversationActions.MarkConversationAsRead(self.conversationId))
end
end
function ConversationView:Update(state)
self.header:SetConnectionState(state.ConnectionState)
local currentConversationId = state.ChatAppReducer.Location.current.parameters.conversationId
local conversation = state.ChatAppReducer.Conversations[currentConversationId]
if not conversation then
return
end
-- The game icon might not exist:
if self.playTogetherGameIcon then
self.playTogetherGameIcon:Update(conversation)
end
if currentConversationId and currentConversationId ~= self.conversationId then
self.conversationId = currentConversationId
self.isFetchingOlderMessages = conversation.fetchingOlderMessages
self.header:SetTitle(getConversationDisplayTitle(conversation))
if self.messageList then
self.messageList:Destruct()
end
local messageList = MessageList.new(self.appState, conversation)
messageList.rbx.LayoutOrder = 3
messageList.rbx.Parent = self.rbx.Contents
messageList:ResizeCanvas()
self.messageList = messageList
if self.messageListConnection ~= nil then
self.messageListConnection:Disconnect()
end
local propertyChangeSignal = self.messageList.rbx:GetPropertyChangedSignal("AbsoluteSize")
self.messageListConnection = propertyChangeSignal:Connect(function()
if self.messageList.isTouchingBottom or self.wasTouchingBottom then
self:TweenScrollToBottom()
self.wasTouchingBottom = false
end
end)
local function onRequestOlderMessages()
local conversationModel = self.appState.store:getState().ChatAppReducer.Conversations[self.conversationId]
if conversationModel == nil then
return
end
local messages = conversationModel.messages
local exclusiveMessageStartId = getNewestWithNilPreviousMessageId(messages)
if conversationModel.fetchingOlderMessages or conversationModel.fetchedOldestMessage then
return
end
self.messageList:StartLoadingMessageHistoryAnimation()
self.appState.store:dispatch(ConversationActions.GetOlderMessages(self.conversationId, exclusiveMessageStartId))
end
if self.requestOlderMessagesConnection then
self.requestOlderMessagesConnection:Disconnect()
end
self.requestOlderMessagesConnection = messageList.RequestOlderMessages:Connect(onRequestOlderMessages)
--Make sure this gets called at least once
onRequestOlderMessages()
if self.readAllMessagesConnection then
self.readAllMessagesConnection:Disconnect()
end
self.readAllMessagesConnection = messageList.ReadAllMessages:Connect(function()
self.appState.store:dispatch(ConversationActions.MarkConversationAsRead(self.conversationId))
end)
if conversation.conversationType == Conversation.Type.ONE_TO_ONE_CONVERSATION then
if self.typingIndicator then
self.typingIndicator:Destruct()
end
local typingIndicator = UserTypingIndicator.new(self.appState, conversation)
typingIndicator.rbx.LayoutOrder = 2
typingIndicator.rbx.Parent = self.rbx.Contents
self.typingIndicator = typingIndicator
typingIndicator.Resized:Connect(function()
self:Rescale()
end)
end
if self.initialLoadingFrame then
self.initialLoadingFrame:Destroy()
end
local initialLoadingFrame = Create.new "Frame" {
Name = "InitialLoadingFrame",
Size = self.messageList.rbx.Size,
Position = self.messageList.rbx.Position,
BackgroundTransparency = 1,
LayoutOrder = 1,
Visible = false
}
initialLoadingFrame.Parent = self.rbx.Contents
self.initialLoadingFrame = initialLoadingFrame
if self.messageList.isTouchingBottom then
self.appState.store:dispatch(ConversationActions.MarkConversationAsRead(self.conversationId))
end
if self.luaChatPlayTogetherEnabled then
self.header:CreateGameDrawer(self.appState.store, self.conversationId, nil, self.appState.analytics)
end
self:Rescale()
elseif conversation == self.oldConversation then
return
end
self.oldConversation = conversation
if not conversation.fetchingOlderMessages then
self.messageList:StopLoadingMessageHistoryAnimation()
end
if conversation.initialLoadingStatus == Constants.ConversationLoadingState.LOADING then
self:StartInitialLoadingAnimation()
else
self:StopInitialLoadingAnimation()
end
self.messageList:Update(conversation)
self.header:SetTitle(getConversationDisplayTitle(conversation))
if LuaChatGroupChatIconEnabled then
if conversation.conversationType == Conversation.Type.MULTI_USER_CONVERSATION then
self.header:SetGroupChatIconVisibility(true)
else
self.header:SetGroupChatIconVisibility(false)
end
end
if self.typingIndicator then
self.typingIndicator:Update(conversation)
end
end
function ConversationView:GetYOffset()
local keyboardSize = 0
if UserInputService.OnScreenKeyboardVisible and self.chatInputBar.textBox:IsFocused() then
keyboardSize = self.rbx.AbsoluteSize.Y - UserInputService.OnScreenKeyboardPosition.Y
end
local offset = keyboardSize
for _, child in ipairs(self.rbx.Contents:GetChildren()) do
if child:IsA("GuiObject") and (self.messageList == nil or child ~= self.messageList.rbx)
and child ~= self.initialLoadingFrame then
offset = offset + child.AbsoluteSize.Y
end
end
return offset
end
function ConversationView:Rescale()
if not self.messageList then
return
end
local offset = self:GetYOffset()
local newSize = UDim2.new(1, 0, 1, -offset)
local wasTouchingBottom = self.messageList.isTouchingBottom
self.messageList.rbx.Size = newSize
if wasTouchingBottom then
self.messageList:ScrollToBottom()
end
self.initialLoadingFrame.Size = newSize
end
function ConversationView:TweenRescale()
if self.messageList == nil then
return
end
local offset = self:GetYOffset()
local newSize = UDim2.new(1, 0, 1, -offset)
self.wasTouchingBottom = self.messageList.isTouchingBottom
self.initialLoadingFrame.Size = newSize
local duration = UserInputService.OnScreenKeyboardAnimationDuration
local tweenInfo = TweenInfo.new(duration)
local propertyGoals = {
Size = newSize,
}
local tween = TweenService:Create(self.messageList.rbx, tweenInfo, propertyGoals)
tween:Play()
end
function ConversationView:TweenScrollToBottom()
local offset = self:GetYOffset()
local height = self.messageList.rbx.CanvasSize.Y.Offset - self.messageList.rbx.AbsoluteWindowSize.Y + offset
local duration = UserInputService.OnScreenKeyboardAnimationDuration
local tweenInfo = TweenInfo.new(duration)
local propertyGoals =
{
CanvasPosition = Vector2.new(0, height)
}
local tween = TweenService:Create(self.messageList.rbx, tweenInfo, propertyGoals)
tween:Play()
end
function ConversationView:StartInitialLoadingAnimation()
if not self.loadingAnimationRunning then
self.loadingAnimationRunning = true
self.messageList.rbx.Visible = false
self.initialLoadingFrame.Visible = true
local loadingIndicator = LoadingIndicator.new(self.appState, 3)
loadingIndicator.rbx.AnchorPoint = Vector2.new(0.5, 0.5)
loadingIndicator.rbx.Position = UDim2.new(0.5, 0, 0.5, 0)
loadingIndicator.rbx.Parent = self.initialLoadingFrame
end
end
function ConversationView:StopInitialLoadingAnimation()
if self.loadingAnimationRunning then
self.loadingAnimationRunning = false
self.messageList.rbx.Visible = true
self.initialLoadingFrame.Visible = false
self.initialLoadingFrame:ClearAllChildren()
end
end
return ConversationView