-
Notifications
You must be signed in to change notification settings - Fork 14
Chat
The chat system is designed as a modular, event-driven UI feature. Its design prioritizes a clear separation of concerns, testability, and scalability. This document outlines the architecture, core components, and development guidelines for contributing to the chat system.
The core architectural goals are:
- Separation of Concerns: Clearly divide UI rendering (Views), UI logic (Presenters), business logic (Commands), and data management (Services).
- Scalability & Maintainability: Make it easy and safe to add new features or modify existing ones without causing ripple effects.
- Clarity: Provide a codebase that is easy for developers to understand and contribute to.
- Testability: Enable unit testing for business logic, reducing reliance on manual testing.
MVP:
- View (MonoBehaviour): draw-only; forwards UI events
- Presenter (POCO): listens to View + EventBus, delegates to Commands/Services, updates View.
- Model: embodied by Services and data stores (e.g., IChatHistory).
- Testability: Enable unit testing for business logic, reducing reliance on manual testing. Commands: single-purpose business logic (e.g., SendMessageCommand). Presenters call commands, commands use services. Services: long-lived, shared state or I/O boundaries (history, member lists, input blocking). EventBus: decoupled comms via ChatEvents (no tight coupling between Presenters). State Machine: ChatStateMachine controls top-level UI mode; replaces boolean soup.
Composition root: ChatMainController wires Views, Presenters, Commands, Services, and State Machine.
The overall UI behavior is controlled by a State Machine (ChatStateMachine). This machine manages the primary states of the chat window (e.g., Focused, Minimized, MembersList). It replaces complex boolean flags with explicit, predictable states, making the UI flow robust and easy to follow.
-
Role: To manage high-level UI states like
Focused,Minimized,Default, etc., replacing complex boolean flag logic. -
Location:
Scripts/Chat/ChatStates -
Key Principle: Each state class (
FocusedChatState.cs, etc.) contains logic only relevant to that state. For example,FocusedChatStateis responsible for blocking player input, whileDefaultChatStateis not. This makes UI behavior explicit and predictable.
Init State
- Internal-only initialization state.
- Not visible to the user.
- Transitions immediately into either Default or Focused state.
Default State (Unfocused State)
- The idle state when the user hasn’t interacted with the chat.
- What is visible:
- Input box visible (unfocused)
- What happens:
- Hover over chat → background, title bar, and panels fade in
- Pointer exits chat → background and panels fade out
- Click inside chat → transitions to Focused
- Click minimize/close → transitions to Minimized
- Click member count (Nearby/Community channels) → transitions to Members
Focused State
- Active chat state when the user is typing or interacting.
- What is visible:
- Title bar visible
- Channel list visible
- Input box focused
- Background fully visible
- What happens:
- Click outside chat → transitions to Default
- Click minimize/close → transitions to Minimized
- Click member count → transitions to Members
Members State
- Displays the list of users in Nearby or Community channels.
- What is visible:
- Only title bar and member list are visible
- Input box, messages, and channel list are hidden
- What happens:
- Click outside chat → transitions to Default
- Click close/back → transitions to Focused
Minimized State
- Collapsed state showing only the unfocused input box.
- What is visible:
- Only input box is visible (unfocused)
- All other UI elements hidden
- What happens:
- Click input box → transitions to Focused
- Click minimize again → transitions to Focused
Hidden State
- Fully invisible chat state used when another panel is open (e.g. Friends).
- What is visible:
- Nothing visible
- Controlled by external UI panels, not user-interactive.
Chat Button Behavior
- If chat is Focused or Default → clicking chat button → transitions to Minimized
- If chat is Minimized → clicking chat button → transitions to Focused
Presenters/Views
- ChatMainController / ChatMainView — composition root + lifecycle (SharedSpaceManager).
- ChatTitlebarPresenter / ChatTitlebarView2
- ChatChannelsPresenter / ChatChannelsView
- ChatMessageFeedPresenter / ChatMessageFeedView
- ChatInputPresenter / ChatInputView
- ChatMemberListPresenter / ChannelMemberFeedView
Commands (selected; keep single-responsibility)
- InitializeChatSystemCommand, RestartChatServicesCommand, ResetChatCommand
- SendMessageCommand, MarkMessagesAsReadCommand
- SelectChannelCommand, OpenConversationCommand, CloseChannelCommand
- GetMessageHistoryCommand, DeleteChatHistoryCommand
- CreateMessageViewModelCommand, CreateChannelViewModelCommand
- GetTitlebarViewModelCommand, GetCommunityThumbnailCommand
- GetChannelMembersCommand, GetParticipantProfilesCommand, GetUserChatStatusCommand
- ResolveInputStateCommand
Services
- CurrentChannelService
- ChatHistoryService
- ChatMemberListService
- ChatInputBlockingService
- ChatContextMenuService
- ChatWorldBubbleService
- ICurrentChannelUserStateService (implemented by CommunityUserStateService, NearbyUserStateService, PrivateConversationUserStateService)
EventBus & Events (Scripts/Chat/ChatEvents.cs)
- Channel lifecycle: InitialChannelsLoadedEvent, ChannelAddedEvent, ChannelUpdatedEvent, ChannelLeftEvent, ChannelSelectedEvent
- History: ChatHistoryClearedEvent
- UI intents: FocusRequestedEvent, CloseChatEvent, ToggleMembersEvent
- Presence: UserStatusUpdatedEvent, ChannelUsersStatusUpdated
- System: ChatResetEvent, CurrentChannelStateUpdatedEvent
- View ChatInputView raises onSubmit("Hello").
- Presenter ChatInputPresenter receives and calls SendMessageCommand.
- Command pulls CurrentChannelService → active channel; calls IChatMessagesBus.
- IChatMessagesBus echoes MessageAdded locally (optimistic UI).
- ChatHistoryService listens → persists to IChatHistory.
- IChatHistory raises MessageAdded.
- Presenter ChatMessageFeedPresenter maps via CreateMessageViewModelCommand → updates View.
- UI shows the message
- ChatMainController creates all Presenters/Commands/Services/StateMachine in OnViewInstantiated.
- Manual DI via constructors (no GetComponent in Presenters).
- Subscribes/unsubscribes to EventBus and disposes resources properly.
- SharedSpaceManager controls visibility; Hidden state used when other panels are foregrounded.
Do
- Keep Views dumb (fields + public events).
- Keep Presenters thin (orchestrate; delegate to Commands/Services).
- Put business logic into Commands/Services.
- Use EventBus for cross-component comms.
- Add a new UI state instead of piling flags.
Don’t
- View ↔ Service direct calls (always via Presenter).
- “Fat Presenters” (extract a Command).
- GetComponent in Presenters (constructor DI only).
- Hidden coupling through static singletons (prefer injected Services).
- Naming: *Presenter, *View, *Command, *Service, *ChatState.
- File locations: match the sections above (e.g., Scripts/Chat/ChatServices).
- Async: prefer UniTask; avoid blocking on main thread; UI updates happen on Unity thread.
- Disposal: Presenters/Services implement IDisposable if they subscribe to events.
- Testing: unit-test commands/services; Presenter tests with mocked Views/Bus/Services.
- Add more Commands for new use-cases (keep inputs/outputs minimal).
- Add States for new modes (e.g., voice overlay) without disturbing Presenters.
- Extend UserState via ICurrentChannelUserStateService implementations.
- Compose new VM commands for view-model mapping (keeps Presenters slim).