diff --git a/.styles/Vocab/Base/accept.txt b/.styles/Vocab/Base/accept.txt index 8212ce1550..be8a7c04e1 100644 --- a/.styles/Vocab/Base/accept.txt +++ b/.styles/Vocab/Base/accept.txt @@ -48,3 +48,10 @@ Async subscribable memoizing Boolean +draggable +sendMessage +overridable +loadMore +pollAnswers +enum +SendMessage diff --git a/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/answer-list-navigation.png b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/answer-list-navigation.png new file mode 100644 index 0000000000..6c549ee94c Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/answer-list-navigation.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/attachment-picker-poll-button.png b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/attachment-picker-poll-button.png new file mode 100644 index 0000000000..d4cf379954 Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/attachment-picker-poll-button.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-answers-list.png b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-answers-list.png new file mode 100644 index 0000000000..0c32c8fe29 Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-answers-list.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-comments-creation-1.png b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-comments-creation-1.png new file mode 100644 index 0000000000..74c8636302 Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-comments-creation-1.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-comments-creation-2.png b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-comments-creation-2.png new file mode 100644 index 0000000000..912e33fb39 Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-comments-creation-2.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-creation-1.png b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-creation-1.png new file mode 100644 index 0000000000..e28a9cea0a Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-creation-1.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-creation-2.png b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-creation-2.png new file mode 100644 index 0000000000..3d4b83a614 Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-creation-2.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-poll-background.png b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-poll-background.png new file mode 100644 index 0000000000..0f06d61e27 Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-poll-background.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-results-title.png b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-results-title.png new file mode 100644 index 0000000000..055d340abd Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-results-title.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-results.png b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-results.png new file mode 100644 index 0000000000..59d1a57ce7 Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-results.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-step-1-1.png b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-step-1-1.png new file mode 100644 index 0000000000..afa0f50eb5 Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-step-1-1.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-step-1-2.png b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-step-1-2.png new file mode 100644 index 0000000000..1b0673793b Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/custom-step-1-2.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/default-ui-1.png b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/default-ui-1.png new file mode 100644 index 0000000000..31a8eb72ae Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/default-ui-1.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/default-ui-2.png b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/default-ui-2.png new file mode 100644 index 0000000000..a86a608472 Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/default-ui-2.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/default-ui-3.png b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/default-ui-3.png new file mode 100644 index 0000000000..59245029c9 Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/default-ui-3.png differ diff --git a/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/enable-polls.png b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/enable-polls.png new file mode 100644 index 0000000000..82fefd237f Binary files /dev/null and b/docusaurus/docs/reactnative/assets/guides/custom-poll-flow/enable-polls.png differ diff --git a/docusaurus/docs/reactnative/common-content/contexts/create-poll-context/close-poll-creation-dialog.mdx b/docusaurus/docs/reactnative/common-content/contexts/create-poll-context/close-poll-creation-dialog.mdx new file mode 100644 index 0000000000..911ae371d4 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/contexts/create-poll-context/close-poll-creation-dialog.mdx @@ -0,0 +1,5 @@ +A method that will be used as a callback whenever the default `CreatePoll` component back button is pressed in the header. Unless used, it will have no effect if the default `CreatePollContent` is overridden. + +| Type | +| ------------ | +| `() => void` | diff --git a/docusaurus/docs/reactnative/common-content/contexts/create-poll-context/create-and-send-poll.mdx b/docusaurus/docs/reactnative/common-content/contexts/create-poll-context/create-and-send-poll.mdx new file mode 100644 index 0000000000..9112949d19 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/contexts/create-poll-context/create-and-send-poll.mdx @@ -0,0 +1,7 @@ +A method that will be used as a callback whenever send button is clicked on the `CreatePoll` modal. If a custom UI is used for `CreatePollContent`, it can be used as an out-of-the-box method to create and send a poll to the `MessageList`. + +It expects to receive an argument that conforms to the `CreatePollData` type that can be found [here](https://github.com/GetStream/stream-chat-js/blob/master/src/types.ts). + +| Type | +| --------------------------------------------- | +| `(pollData: CreatePollData) => Promise` | diff --git a/docusaurus/docs/reactnative/common-content/contexts/create-poll-context/create-poll-content.mdx b/docusaurus/docs/reactnative/common-content/contexts/create-poll-context/create-poll-content.mdx new file mode 100644 index 0000000000..23d12f6573 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/contexts/create-poll-context/create-poll-content.mdx @@ -0,0 +1,5 @@ +A custom UI component used to render the entire poll creation form. It has access to the [`CreatePollContext`](../../../contexts/create-poll-context.mdx) values by default through the `useCreatePollContext` hook. + +| Type | Default | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`CreatePollContent`](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Poll/CreatePollContent.tsx) | diff --git a/docusaurus/docs/reactnative/common-content/contexts/create-poll-context/create-poll-option-height.mdx b/docusaurus/docs/reactnative/common-content/contexts/create-poll-context/create-poll-option-height.mdx new file mode 100644 index 0000000000..c9a1e65bc7 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/contexts/create-poll-context/create-poll-option-height.mdx @@ -0,0 +1,5 @@ +A property used to define the height of the poll options in the `CreatePollContent` draggable list. The items can have a constant and equal height only and this should only be used if custom theming implies that the static height of the items changes. Has no effect if a custom UI for `CreatePollContent` is provided. + +| Type | +| ------ | +| number | diff --git a/docusaurus/docs/reactnative/common-content/contexts/message-input-context/close-poll-creation-dialog.mdx b/docusaurus/docs/reactnative/common-content/contexts/message-input-context/close-poll-creation-dialog.mdx new file mode 100644 index 0000000000..0c885ef614 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/contexts/message-input-context/close-poll-creation-dialog.mdx @@ -0,0 +1,5 @@ +Function called whenever the close button is pressed on the poll creation modal. Has no effect if [`PollCreateContent`](../../../core-components/channel.mdx#createpollcontent) is custom. + +| Type | +| -------- | +| function | diff --git a/docusaurus/docs/reactnative/common-content/contexts/message-input-context/show-poll-creation-dialog.mdx b/docusaurus/docs/reactnative/common-content/contexts/message-input-context/show-poll-creation-dialog.mdx new file mode 100644 index 0000000000..00bf17abc6 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/contexts/message-input-context/show-poll-creation-dialog.mdx @@ -0,0 +1,5 @@ +A boolean signifying whether the poll creation dialog is shown or not. Will always be `false` if [`PollCreateContent`](../../../core-components/channel.mdx#createpollcontent) is custom. + +| Type | +| ------- | +| boolean | diff --git a/docusaurus/docs/reactnative/common-content/contexts/poll-context/poll.mdx b/docusaurus/docs/reactnative/common-content/contexts/poll-context/poll.mdx new file mode 100644 index 0000000000..ad66595ea1 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/contexts/poll-context/poll.mdx @@ -0,0 +1,13 @@ +An instance of the [`Poll` class](https://github.com/GetStream/stream-chat-js/blob/master/src/poll.ts) containing reactive state. + +It is used by the underlying `usePollContext`, `usePollStateStore` and `usePollState` hooks to provide us with the reactive `Poll` state. + +:::note + +If you need the `Poll` instance you may get it from `client.polls.fromState(pollId)`. + +::: + +| Type | +| ------ | +| object | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/has-create-poll.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/has-create-poll.mdx new file mode 100644 index 0000000000..c7502ec7d0 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/has-create-poll.mdx @@ -0,0 +1,5 @@ +A boolean value that is responsible for controlling whether the poll creation button is visible or not. + +| Type | +| ------- | +| boolean | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/channel/props/open-poll-creation-dialog.mdx b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/open-poll-creation-dialog.mdx new file mode 100644 index 0000000000..7ca547b505 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/ui-components/channel/props/open-poll-creation-dialog.mdx @@ -0,0 +1,11 @@ +Function called immediately when the poll creation button is clicked in the attachment picker. Can be used to override the default behaviour of the poll creation UI appearing as a Modal. + +If overridden, a `payload` is passed containing the `sendMessage` callback from [`MessageInputContext`](../../../../contexts/message-input-context.mdx) is passed, so that [`CreatePoll`](../../../../ui-components/create-poll.mdx) + +| Type | +| -------- | +| function | + +| Parameter | Description | +| --------- | ----------------- | +| payload | `{ sendMessage }` | diff --git a/docusaurus/docs/reactnative/common-content/ui-components/poll/poll-content.mdx b/docusaurus/docs/reactnative/common-content/ui-components/poll/poll-content.mdx new file mode 100644 index 0000000000..63f0e11163 --- /dev/null +++ b/docusaurus/docs/reactnative/common-content/ui-components/poll/poll-content.mdx @@ -0,0 +1,25 @@ +A `Component` prop used to render the content of the `Poll` component in `MessageList`. + +The component has full access to the entire `Poll` reactive state through the `usePollState` hook. + +| Type | Default | +| ------------- | --------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`PollContent`](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Poll/Poll.tsx) | + +#### Props + +##### `PollHeader` + +A `Component` prop used to render the header of the `PollContent` component. + +| Type | Default | +| ------------- | -------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`PollHeader`](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Poll/Poll.tsx) | + +##### `PollButtons` + +A `Component` prop used to render the buttons of the `PollContent` component. + +| Type | Default | +| ------------- | --------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`PollButtons`](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Poll/Poll.tsx) | diff --git a/docusaurus/docs/reactnative/contexts/create-poll-context.mdx b/docusaurus/docs/reactnative/contexts/create-poll-context.mdx new file mode 100644 index 0000000000..a852571480 --- /dev/null +++ b/docusaurus/docs/reactnative/contexts/create-poll-context.mdx @@ -0,0 +1,50 @@ +--- +id: create-poll-context +title: CreatePollContext +--- + +import ClosePollCreationDialog from '../common-content/contexts/create-poll-context/close-poll-creation-dialog.mdx'; +import CreateAndSendPoll from '../common-content/contexts/create-poll-context/create-and-send-poll.mdx'; +import CreatePollOptionHeight from '../common-content/contexts/create-poll-context/create-poll-option-height.mdx'; +import SendMessage from '../common-content/contexts/message-input-context/send_message.mdx'; + +The `CreatePollContext` is provided by the [`CreatePoll`](../../ui-components/create-poll) component. If you are not familiar with React Context API, please read about it on [React docs](https://reactjs.org/docs/context.html). + +The `CreatePollContext` needs to be used within the [`Channel` component](../../core-components/channel) as it depends on the [`MessageInputContext`](./message-input-context.mdx) which is added by it. + +## Basic Usage + +The `CreatePollContext` can be consumed by any of the child components of `Channel` as following: + +```tsx +import { useContext } from 'react'; +import { CreatePollContext } from 'stream-chat-react-native'; + +const value = useContext(CreatePollContext); +``` + +Alternatively, you can also use the `useCreatePollContext` hook provided by library to consume `CreatePollContext`. + +```tsx +import { useCreatePollContext } from 'stream-chat-react-native'; + +const value = useCreatePollContext(); +``` + +## Value + +###
_forwarded from [MessageInputContext](../../contexts/message-input-context#sendmessage)_ props
sendMessage {#sendmessage} + + + +### `closePollCreationDialog` + + + +### `createAndSendPoll` + + + +### `createPollOptionHeight` + + diff --git a/docusaurus/docs/reactnative/contexts/message-input-context.mdx b/docusaurus/docs/reactnative/contexts/message-input-context.mdx index 73319587c6..b73f013bc0 100644 --- a/docusaurus/docs/reactnative/contexts/message-input-context.mdx +++ b/docusaurus/docs/reactnative/contexts/message-input-context.mdx @@ -94,6 +94,8 @@ import UploadImage from '../common-content/contexts/message-input-context/upload import UpdateMessage from '../common-content/contexts/message-input-context/update_message.mdx'; import UploadNewFile from '../common-content/contexts/message-input-context/upload_new_file.mdx'; import UploadNewImage from '../common-content/contexts/message-input-context/upload_new_image.mdx'; +import ClosePollCreationDialog from '../common-content/contexts/message-input-context/close-poll-creation-dialog.mdx'; +import ShowPollCreationDialog from '../common-content/contexts/message-input-context/show-poll-creation-dialog.mdx'; `MessageInputContext` is provided by [`Channel`](../../core-components/channel) component. If you are not familiar with React Context API, please read about it on [React docs](https://reactjs.org/docs/context.html). @@ -490,3 +492,11 @@ const { sendMessage, toggleAttachmentPicker } = useMessageInputContext(); ###
_forwarded from [Channel](../../core-components/channel#uploadprogressindicator)_ props
UploadProgressIndicator {#uploadprogressindicator} + +### `closePollCreationDialog` + + + +### `showPollCreationDialog` + + diff --git a/docusaurus/docs/reactnative/contexts/messages-context.mdx b/docusaurus/docs/reactnative/contexts/messages-context.mdx index 5ace299ae9..626cfb0602 100644 --- a/docusaurus/docs/reactnative/contexts/messages-context.mdx +++ b/docusaurus/docs/reactnative/contexts/messages-context.mdx @@ -40,7 +40,6 @@ import InitialScrollToFirstUnreadMessage from '../common-content/ui-components/c import InlineDateSeparator from '../common-content/ui-components/channel/props/inline_date_separator.mdx'; import InlineUnreadIndicator from '../common-content/ui-components/channel/props/inline_unread_indicator.mdx'; import IsAttachmentEqual from '../common-content/ui-components/channel/props/is_attachment_equal.mdx'; -import LegacyImageViewerSwipeBehaviour from '../common-content/ui-components/channel/props/legacy_image_viewer_swipe_behaviour.mdx'; import MarkdownRules from '../common-content/ui-components/channel/props/markdown_rules.mdx'; import MessageAvatar from '../common-content/ui-components/channel/props/message-avatar.mdx'; import MessageBounce from '../common-content/ui-components/channel/props/message-bounce.mdx'; @@ -73,6 +72,9 @@ import TypingIndicator from '../common-content/ui-components/channel/props/typin import TypingIndicatorContainer from '../common-content/ui-components/channel/props/typing_indicator_container.mdx'; import UrlPreview from '../common-content/ui-components/channel/props/url_preview.mdx'; import VideoThumbnail from '../common-content/ui-components/channel/props/video_thumbnail.mdx'; +import OpenPollCreationDialog from '../common-content/ui-components/channel/props/open-poll-creation-dialog.mdx'; +import HasCreatePoll from '../common-content/ui-components/channel/props/has-create-poll.mdx'; +import PollContent from '../common-content/ui-components/poll/poll-content.mdx'; ## Value @@ -260,6 +262,14 @@ Upserts a given message in local channel state. Please note that this function d | ------------------- | | `(message) => void` | +###
_forwarded from [Channel](../../core-components/channel#openpollcreationdialog)_ props
openPollCreationDialog {#openPollCreationDialog} + + + +###
_forwarded from [Channel](../../core-components/channel#hascreatepoll)_ props
hasCreatePoll {#hascreatepoll} + + + ###
_forwarded from [Channel](../../core-components/channel#attachment)_ props
Attachment {#attachment} @@ -419,3 +429,7 @@ Upserts a given message in local channel state. Please note that this function d ###
_forwarded from [Channel](../../core-components/channel#videothumbnail)_ props
VideoThumbnail {#videoThumbnail} + +###
_forwarded from [Channel](../../core-components/channel#pollcontent)_ props
PollContent {#pollcontent} + + diff --git a/docusaurus/docs/reactnative/contexts/own-capabilities-context.mdx b/docusaurus/docs/reactnative/contexts/own-capabilities-context.mdx index 1e60f34094..143b902d12 100644 --- a/docusaurus/docs/reactnative/contexts/own-capabilities-context.mdx +++ b/docusaurus/docs/reactnative/contexts/own-capabilities-context.mdx @@ -148,3 +148,27 @@ Ability to upload message attachments. | Type | | ------- | | boolean | + +### `sendPoll` + +Ability to send polls as messages. + +| Type | +| ------- | +| boolean | + +### `queryPollVotes` + +Ability to query for the votes of polls. + +| Type | +| ------- | +| boolean | + +### `castPollVote` + +Ability to cast a vote on polls. + +| Type | +| ------- | +| boolean | diff --git a/docusaurus/docs/reactnative/contexts/poll-context.mdx b/docusaurus/docs/reactnative/contexts/poll-context.mdx new file mode 100644 index 0000000000..fa2dbc2335 --- /dev/null +++ b/docusaurus/docs/reactnative/contexts/poll-context.mdx @@ -0,0 +1,40 @@ +--- +id: poll-context +title: PollContext +--- + +import MessageProp from '../common-content/contexts/message-context/message.mdx'; +import Poll from '../common-content/contexts/poll-context/poll.mdx'; + +The `PollContext` is provided by the [`Poll`](../../ui-components/poll) component. If you are not familiar with React Context API, please read about it on [React docs](https://reactjs.org/docs/context.html). + +The `PollContext` needs to be used within the [`Channel` component](../../core-components/channel) as it depends on various values provided by it in order to work. + +## Basic Usage + +The `PollContext` can be consumed by any of the child components of `Channel` as following: + +```tsx +import { useContext } from 'react'; +import { CreatePollContext } from 'stream-chat-react-native'; + +const value = useContext(PollContext); +``` + +Alternatively, you can also use the `usePollContext` hook provided by library to consume `PollContext`. + +```tsx +import { usePollContext } from 'stream-chat-react-native'; + +const value = usePollContext(); +``` + +## Value + +###
_forwarded from [MessageContext](../../contexts/message-context#message)_ props
message {#message} + + + +### `poll` \* + + diff --git a/docusaurus/docs/reactnative/core-components/channel.mdx b/docusaurus/docs/reactnative/core-components/channel.mdx index cc3c35334a..21a19744f9 100644 --- a/docusaurus/docs/reactnative/core-components/channel.mdx +++ b/docusaurus/docs/reactnative/core-components/channel.mdx @@ -144,6 +144,10 @@ import ThreadList from '../common-content/ui-components/channel/props/thread_lis import TypingIndicator from '../common-content/ui-components/channel/props/typing_indicator.mdx'; import TypingIndicatorContainer from '../common-content/ui-components/channel/props/typing_indicator_container.mdx'; import UrlPreview from '../common-content/ui-components/channel/props/url_preview.mdx'; +import CreatePollContent from '../common-content/contexts/create-poll-context/create-poll-content.mdx'; +import OpenPollCreationDialog from '../common-content/ui-components/channel/props/open-poll-creation-dialog.mdx'; +import HasCreatePoll from '../common-content/ui-components/channel/props/has-create-poll.mdx'; +import PollContent from '../common-content/ui-components/poll/poll-content.mdx'; The `Channel` component is the main entry point for many Stream Chat customizations and contains the majority of the content providers and a significant amount of the logic utilized by Stream Chat for React Native. Components, custom and out of the box, channel information, and UI integrated chat functions are disseminated by the contexts contained in `Channel`. @@ -727,6 +731,14 @@ Callback function to set the [ref](https://reactjs.org/docs/refs-and-the-dom.htm +### `openPollCreationDialog` + + + +### `hasCreatePoll` + + + ## UI Components Props @@ -1028,3 +1040,11 @@ Component to render upload progress indicator as an overlay above [ImageUploadPr ### UrlPreview + +### CreatePollContent + + + +### PollContent + + diff --git a/docusaurus/docs/reactnative/guides/custom-poll-flow.mdx b/docusaurus/docs/reactnative/guides/custom-poll-flow.mdx new file mode 100644 index 0000000000..8d7495ce30 --- /dev/null +++ b/docusaurus/docs/reactnative/guides/custom-poll-flow.mdx @@ -0,0 +1,725 @@ +--- +id: custom-poll-flow +title: Custom Poll Flow +--- + +import DefaultUI1 from '../assets/guides/custom-poll-flow/default-ui-1.png'; +import DefaultUI2 from '../assets/guides/custom-poll-flow/default-ui-2.png'; +import DefaultUI3 from '../assets/guides/custom-poll-flow/default-ui-3.png'; + +import CustomStep11 from '../assets/guides/custom-poll-flow/custom-step-1-1.png'; +import CustomStep12 from '../assets/guides/custom-poll-flow/custom-step-1-2.png'; + +import CustomCreation1 from '../assets/guides/custom-poll-flow/custom-creation-1.png'; +import CustomCreation2 from '../assets/guides/custom-poll-flow/custom-creation-2.png'; + +import CustomCommentsCreation1 from '../assets/guides/custom-poll-flow/custom-comments-creation-1.png'; +import CustomCommentsCreation2 from '../assets/guides/custom-poll-flow/custom-comments-creation-2.png'; + +In this cookbook we'll go over how we can create and customize a `Channel` screen containing the [`Poll` component](../ui-components/poll.mdx), along with adding navigation to all of its underlying screens rather than relying on modals. + +### Prerequisites + +This cookbook assumes that you've already set up a screen that is able to display a `Channel` with a `MessageList` inside. As `poll`s are tightly coupled with messages, we need to have this working before we add the `poll` message type. + +Polls are disabled by default and can be enabled through the Stream Dashboard by toggling the `Poll` button. + +![Enable Polls](../assets/guides/custom-poll-flow/enable-polls.png) + +Doing that will include the `Poll` button in your attachment picker: + +![Enable Polls](../assets/guides/custom-poll-flow/attachment-picker-poll-button.png) + +It also assumes that you have a functioning `chatClient` that is used elsewhere. + +For illustration purposes, we are going to be using the `React Navigation` library for navigation (however, any navigation library can be used). + +### The initial UI + +Without doing any form of customization yet, we should have something similar to the following: + +```tsx +import { OverlayProvider, Chat, Channel, MessageList, MessageInput } from 'stream-chat-react-native'; + +const ChannelScreen = () => { + return ( + + + + + + + + + + ); +}; +``` + +and the `poll` creation flow should come out of the box with its default UI: + +
+ Default UI 1 + Default UI 2 + Default UI 3 +
+ +At this point, let's try to customize the `Poll` component within the message. To do this, we can set the `PollContent` prop on our `Channel` component. + +For now, we want to remove the `Poll` header and modify the behaviour of the buttons at the bottom. To do this, we can reuse the default `PollContent` component exported by the SDK that allows us to do modifications. + +Additionally, we can reuse all of the buttons that we need and modify the ones that we want to do something else. + +```tsx +import { + OverlayProvider, + Chat, + Channel, + MessageList, + MessageInput, + // highlight-next-line + PollContent, +} from 'stream-chat-react-native'; + +// highlight-start +const MyPollButtons = () => { + return ( + <> + + Alert.alert(`Message ID: ${message.id} and Poll ID: ${poll.id}`)} + /> + + + ); +}; + +const MyPollContent = () => null} PollButtons={MyPollButtons} />; +// highlight-end + +const ChannelScreen = () => { + return ( + + + // highlight-next-line + + + + + + + + ); +}; +``` + +
+ Step 1-1 + Step 1-2 +
+ +As we can see, now we only display 2 buttons at the bottom, which are the `View Results` and `End Vote` ones. Clicking on the `View Results` button displays an `Alert` with some information about the `Poll` instead of opening the modal. + +At this point we might realize that we no longer have access to the `PollResults` screen anymore. So, let's bring it back. However, we do not want to use the default navigation which is based on [React Native Modals](https://reactnative.dev/docs/modal), but rather introduce our own navigation. + +To do this, let's rely on `React Navigation`. Since we need all of the `Poll` screens to also be children of the `Channel` component, we can introduce an additional `Stack` navigator that would take care of this. + +```tsx +import { OverlayProvider, Chat, Channel, MessageList, MessageInput, PollContent } from 'stream-chat-react-native'; +// highlight-next-line +import { createStackNavigator } from '@react-navigation/stack'; + +const MyPollButtons = () => { + return ( + <> + + Alert.alert(`Message ID: ${message.id} and Poll ID: ${poll.id}`)} + /> + + + ); +}; + +const MyPollContent = () => null} PollButtons={MyPollButtons} />; + +// highlight-start +const ChannelMessageList = () => { + return ( + <> + + + + + ); +}; +// highlight-end + +// highlight-next-line +const ChannelStack = createStackNavigator(); + +const ChannelScreen = () => { + return ( + + + + // highlight-start + + + null} /> + + // highlight-end + + + + ); +}; +``` + +For now, we'll leave the `PollResultsScreen` empty. With the code above, we're allowed to create a new navigation stack within the `Channel` component that we can then use for all of the `Poll` screens. + +It would allow us to not have to move the `channel` prop around (as well as all other `Channel` customizations we might have) and still handle everything with our own navigation. + +As a next step, let's reconstruct the `PollResults` screen. For this, we may use the `PollModalHeader` and `PollResults` components and get the default UI out of the box. + +```tsx +import { + OverlayProvider, + Chat, + Channel, + MessageList, + MessageInput, + PollContent, + // highlight-start + PollResults, + PollModalHeader, + // highlight-end +} from 'stream-chat-react-native'; +import { createStackNavigator } from '@react-navigation/stack'; + +const MyPollButtons = () => { + return ( + <> + + + navigation.navigate('PollResultsScreen', { + message, + poll, + }); + } + /> + + + ) +} + +// ... rest of the components + +// highlight-start +const PollResultsScreen = ({ + route: { + params: { message, poll }, + }, +}) => { + const navigation = useNavigation(); + return ( + + navigation.goBack()} /> + + + ); +}; +// highlight-end + +const ChannelScreen = () => { + return ( + + + + + + + + + + + ); +}; +``` + +Clicking on the `View Results` button now will successfully navigate us to the `PollResults` screen. It is no longer a modal but rather part of the navigation stack that we introduced. + +![Custom Results](../assets/guides/custom-poll-flow/custom-results.png) + +It looks exactly the same as the default UI, however the title is changed to `RESULTS` rather than the `Poll Results`. + +As a next step, let's say that we want to include the `Poll` name pinned at the top so it's present all the time in case we get many options. + +For this purpose, we can use the `usePollState` hook and the `PollResultsContent` component. + +```tsx +import { + // ...rest of the imports + // highlight-start + usePollState, + PollResultsContent, + // highlight-end +} from 'stream-chat-react-native'; +import { createStackNavigator } from '@react-navigation/stack'; + +// ... rest of the components + +// highlight-start +const MyPollResultsContent = () => { + const { name } = usePollState(); + const navigation = useNavigation(); + return ( + <> + navigation.goBack()} /> + + + ); +}; + +const PollResultsScreen = ({ + route: { + params: { message, poll }, + }, +}) => { + return ( + + + + ); +}; +// highlight-end + +const ChannelScreen = () => { + return ( + + + + + + + + + + + ); +}; +``` + +Providing us with the following UI: + +![Custom Results Title](../assets/guides/custom-poll-flow/custom-results-title.png) + +We are indeed allowed to do this because `PollResults` comes pre-wrapped within a `PollContext` and the button `onPress` callback provides us with all of the data we need to render initialize the `PollContextProvider`. + +Since we did this to the results screen, let's do the same for the poll creation one for consistency. + +```tsx +import { + // ...rest of the imports + // highlight-next-line + CreatePoll, +} from 'stream-chat-react-native'; +import { createStackNavigator } from '@react-navigation/stack'; + +// ... rest of the components + +// highlight-start +const MyCreatePollContent = ({ + route: { + params: { sendMessage }, + }, +}) => { + const navigation = useNavigation(); + return ( + + navigation.goBack()} /> + + ); +}; +// highlight-end + +const ChannelScreen = () => { + // highlight-next-line + const navigation = useNavigation(); + return ( + + + navigation.navigate('CreatePollScreen', { sendMessage })} + // highlight-end + > + + + + // highlight-start + + + + // highlight-end + + + + + ); +}; +``` + +giving us the final UI: + +
+ Step 1-1 + Step 1-2 +
+ +Going forward with customizations, let's change the background color of our `Poll`s. Since all `Poll` components are `theme` compatible, we can do this by overriding the default theme: + +```tsx +import { + // ...rest of the imports + // highlight-next-line + ThemeProvider, +} from 'stream-chat-react-native'; +import { createStackNavigator } from '@react-navigation/stack'; + +// ... rest of the components + +// highlight-start +const myTheme: DeepPartial = { + poll: { + message: { + container: { + backgroundColor: 'pink', + }, + }, + }, +}; +// highlight-end + +const ChannelScreen = () => { + const navigation = useNavigation(); + return ( + // highlight-next-line + + + + navigation.navigate('CreatePollScreen', { sendMessage })} + > + + + + + + + + + + + // highlight-next-line + + ); +}; +``` + +which gives us a changed `Poll` background: + +![Custom Poll Background](../assets/guides/custom-poll-flow/custom-poll-background.png) + +:::note +Make sure that the `theme` is set above your `OverlayProvider` to make sure that the `Poll` customizations are also reflected on the message preview whenever it's long pressed. +::: + +As one final step, let's assume we want to add an `PollAnswersList`; however the default UI doesn't fit our requirements and we want something more custom. + +To do this, we first need to add the `ShowAllCommentsButton` and `AddCommentButton` components to our custom `Buttons` UI: + +```tsx +import { + // ...rest of the imports + // highlight-start + ShowAllCommentsButton, + AddCommentButton + // highlight-end +} from 'stream-chat-react-native'; +import { createStackNavigator } from '@react-navigation/stack'; + +// ... rest of the components + +const MyPollButtons = () => { + return ( + <> + + // highlight-start + + + // highlight-end + + navigation.navigate('PollResultsScreen', { + message, + poll, + }); + } + /> + + + ) +} + +// ... the ChannelScreen component +``` + +Next, to actually create comments we need a poll that accepts answers (comments) as well as adding a comment. We can do this by enabling the option when creating a poll and then clicking on the `Add Comment` button: + +
+ Step 1-1 + Step 1-2 +
+ +Now, similar to how we handled the other screens we would want to create a separate `Screen` in the navigation stack for our `AnswersList`. For now, we'll use the default UI: + +```tsx +import { + // ...rest of the imports + // highlight-next-line + PollAnswersList, +} from 'stream-chat-react-native'; +import { createStackNavigator } from '@react-navigation/stack'; + +// ... rest of the components + +const MyPollButtons = () => { + return ( + <> + + // highlight-start + { + navigation.navigate('PollAnswersScreen', { + message, + poll, + }); + }} + /> + // highlight-end + + + navigation.navigate('PollResultsScreen', { + message, + poll, + }); + } + /> + + + ) +} + +// highlight-start +const PollAnswersScreen = ({ + route: { + params: { message, poll }, + }, +}) => { + const navigation = useNavigation(); + return ( + + navigation.goBack()} /> + + + ); +}; +// highlight-end + +const ChannelScreen = () => { + const navigation = useNavigation(); + return ( + + + + navigation.navigate('CreatePollScreen', { sendMessage })} + > + + + + // highlight-start + + // highlight-end + + + + + + + + + ); +}; +``` + +which will give allow us to navigate to the default `PollAnswersList` UI: + +![Answer List Navigation](../assets/guides/custom-poll-flow/answer-list-navigation.png) + +Now, let's finally customize the UI. To achieve this we can override the `PollAnswersListContent` of our `PollAnswersList`. + +Since the list of answers can be very large and we want to be able to still display all answers, we will use the `usePollAnswersPagination` hook to get them: + +```tsx +import { + // ...rest of the imports + // highlight-next-line + usePollAnswersPagination, +} from 'stream-chat-react-native'; +import { createStackNavigator } from '@react-navigation/stack'; + +// ... rest of the components + +// highlight-start +const LoadingIndicator = () => { + /* some LoadingIndicator logic here */ +}; + +const MyItem = ({ item }) => { + const { answer_text, user } = item; + return ( + + {user.name} commented: {answer_text} + + ); +}; + +const MyPollAnswersContent = () => { + const { pollAnswers, loading, loadMore } = usePollAnswersPagination(); + return ( + : null} + /> + ); +}; +// highlight-end + +const PollAnswersScreen = ({ + route: { + params: { message, poll }, + }, +}) => { + const navigation = useNavigation(); + return ( + + navigation.goBack()} /> + // highlight-start + + // highlight-end + + ); +}; + +// ... the Channel screen +``` + +And we get the final content: + +![Custom Answers List](../assets/guides/custom-poll-flow/custom-answers-list.png) + +The list will be fully compatible with loading the pagination when scrolling to the bottom and displaying a loading indicator whenever that happens as well. + +With that, we have finished the customizations we wanted to do for our polls. + +As a last note; any components that you'd like to reuse from the default UI are free to be imported from within the SDK. + +An extensive list of these includes: + +- All buttons mentioned [here](../ui-components/poll-buttons.mdx) +- `CreatePollContent` +- `PollContent` +- `PollButtons` +- `PollHeader` +- `PollModalHeader` +- `PollInputDialog` +- `CreatePollIcon` +- `PollOption` +- `PollResultsContent` +- `PollResultsItem` +- `PollVote` +- `PollAllOptionsContent` diff --git a/docusaurus/docs/reactnative/guides/custom-thread-list.mdx b/docusaurus/docs/reactnative/guides/custom-thread-list.mdx index 5ba4de2916..665e3b5624 100644 --- a/docusaurus/docs/reactnative/guides/custom-thread-list.mdx +++ b/docusaurus/docs/reactnative/guides/custom-thread-list.mdx @@ -3,8 +3,6 @@ id: custom-thread-list title: Custom Thread List --- -import loadMoreButton from '../assets/guides/custom-channel-list/load_more_button.png'; - In this cookbook we'll go over how we can create and customize a screen containing the [`ThreadList` component](../ui-components/thread-list.mdx). The goal will be to display a list of threads as well as have a banner that displays the number of unread threads within the list. ### Prerequisites diff --git a/docusaurus/docs/reactnative/hooks/poll/use-create-poll-context.mdx b/docusaurus/docs/reactnative/hooks/poll/use-create-poll-context.mdx new file mode 100644 index 0000000000..e0688daf80 --- /dev/null +++ b/docusaurus/docs/reactnative/hooks/poll/use-create-poll-context.mdx @@ -0,0 +1,6 @@ +--- +id: use-create-poll-context +title: useCreatePollContext +--- + +A hook that returns the [`CreatePollContext` value](../../contexts/create-poll-context.mdx#value). diff --git a/docusaurus/docs/reactnative/hooks/poll/use-poll-answers-pagination.mdx b/docusaurus/docs/reactnative/hooks/poll/use-poll-answers-pagination.mdx new file mode 100644 index 0000000000..02b7104ded --- /dev/null +++ b/docusaurus/docs/reactnative/hooks/poll/use-poll-answers-pagination.mdx @@ -0,0 +1,78 @@ +--- +id: use-poll-answers-pagination +title: usePollAnswersPagination +--- + +A hook that queries answers for a given `Poll` and returns them in a paginated fashion. It can be used whenever we want to view all answers in a given `poll`, like in the [`PollAnswersList` component](../../ui-components/poll-answers-list.mdx) for example. + +## Properties + +### `loadFirstPage` + +A property that depicts whether the hook should load the first page of answers when first invoked or not. Default to `true` if omitted. + +| Type | +| --------- | +| `boolean` | + +### `paginationParams` + +An object containing the pagination parameters that we may want to pass to the hook that will then be used to query for answers. + +Its type can be found [here](https://github.com/GetStream/stream-chat-js/blob/master/src/types.ts) under the name `PollAnswersQueryParams`. + +| Type | +| -------- | +| `object` | + +## Return type + +The `usePollAnswersPagination` hook returns an object containing pagination values for the `poll`'s answers. + +### error + +An object containing an error if one is thrown during querying for the answers. + +| Type | +| -------- | +| `object` | + +### hasNextPage + +A property depicting whether the list of answers has a next page to load or not. + +| Type | +| --------- | +| `boolean` | + +### loading + +A property depicting whether the next page of answers is currently loading or not. + +| Type | +| --------- | +| `boolean` | + +### loadMore + +A function that loads the next page of answers whenever invoked. It will automatically update [`pollAnswers`](#pollanswers) + +| Type | +| ------------ | +| `() => void` | + +### next + +A reference to the next cursor in the pagination. + +| Type | +| -------- | +| `string` | + +### pollAnswers + +A list of answers that has been loaded so far. It automatically updates itself whenever `loadMore()` is invoked and triggers a `state` update. + +| Type | +| -------------- | +| `PollAnswer[]` | diff --git a/docusaurus/docs/reactnative/hooks/poll/use-poll-context.mdx b/docusaurus/docs/reactnative/hooks/poll/use-poll-context.mdx new file mode 100644 index 0000000000..d7046a7a20 --- /dev/null +++ b/docusaurus/docs/reactnative/hooks/poll/use-poll-context.mdx @@ -0,0 +1,6 @@ +--- +id: use-poll-context +title: usePollContext +--- + +A hook that returns the [`PollContext` value](../../contexts/poll-context.mdx#value). diff --git a/docusaurus/docs/reactnative/hooks/poll/use-poll-option-votes-pagination.mdx b/docusaurus/docs/reactnative/hooks/poll/use-poll-option-votes-pagination.mdx new file mode 100644 index 0000000000..fb932bff7e --- /dev/null +++ b/docusaurus/docs/reactnative/hooks/poll/use-poll-option-votes-pagination.mdx @@ -0,0 +1,88 @@ +--- +id: use-poll-option-votes-pagination +title: usePollOptionVotesPagination +--- + +A hook that queries votes for a given option in a `Poll` and returns them in a paginated fashion. It can be used whenever we want to view all votes for an option in a given `poll`, like in the [`PollOptionFullResults` component](../../ui-components/poll-option-full-results.mdx) for example. + +## Properties + +### `loadFirstPage` + +A property that depicts whether the hook should load the first page of answers when first invoked or not. Default to `true` if omitted. + +| Type | +| --------- | +| `boolean` | + +### `option` \* + +The `poll` option that we want to query the votes for. + +| Type | +| -------- | +| `object` | + +### `paginationParams` + +An object containing the pagination parameters that we may want to pass to the hook that will then be used to query for votes. + +Its type can be found [here](https://github.com/GetStream/stream-chat-js/blob/master/src/types.ts) under the name `PollOptionVotesQueryParams`. + +The `{ option_id: }` filter is mandatory and will be added regardless of overrides. + +| Type | +| -------- | +| `object` | + +## Return type + +The `usePollOptionVotesPagination` hook returns an object containing pagination values for the `poll` option's votes. + +### error + +An object containing an error if one is thrown during querying for the votes. + +| Type | +| -------- | +| `object` | + +### hasNextPage + +A property depicting whether the list of votes has a next page to load or not. + +| Type | +| --------- | +| `boolean` | + +### loading + +A property depicting whether the next page of votes is currently loading or not. + +| Type | +| --------- | +| `boolean` | + +### loadMore + +A function that loads the next page of votes whenever invoked. It will automatically update [`votes`](#votes) + +| Type | +| ------------ | +| `() => void` | + +### next + +A reference to the next cursor in the pagination. + +| Type | +| -------- | +| `string` | + +### vote + +A list of votes that has been loaded so far. It automatically updates itself whenever `loadMore()` is invoked and triggers a `state` update. + +| Type | +| ------------ | +| `PollVote[]` | diff --git a/docusaurus/docs/reactnative/hooks/poll/use-poll-state-store.mdx b/docusaurus/docs/reactnative/hooks/poll/use-poll-state-store.mdx new file mode 100644 index 0000000000..58e756c5a2 --- /dev/null +++ b/docusaurus/docs/reactnative/hooks/poll/use-poll-state-store.mdx @@ -0,0 +1,31 @@ +--- +id: use-poll-state-store +title: usePollStateStore +--- + +A utility hook used to access the `poll` state store directly by passing a [`selector`](../../state-and-offline-support/state-overview.mdx#what-are-selectors). + +The full extent of the `Poll` state can be found [here](https://github.com/GetStream/stream-chat-js/blob/master/src/poll.ts), under the `PollState` type. + +This hook can only be used within a child of [`PollContext`](../../contexts/poll-context.mdx). + +## Example usage + +```tsx +import { PollVote, PollState } from 'stream-chat'; +import { usePollStateStore } from 'stream-chat-react-native'; + +type PollOptionSelectorReturnValue = { + latest_votes_by_option: Record; + maxVotedOptionIds: string[]; +}; + +const selector = ( + nextValue: PollState, +): PollOptionSelectorReturnValue => ({ + latest_votes_by_option: nextValue.latest_votes_by_option, + maxVotedOptionIds: nextValue.maxVotedOptionIds, +}); + +const { latest_votes_by_option, maxVotedOptionIds } = usePollStateStore(selector); +``` diff --git a/docusaurus/docs/reactnative/hooks/poll/use-poll-state.mdx b/docusaurus/docs/reactnative/hooks/poll/use-poll-state.mdx new file mode 100644 index 0000000000..e35a75d4b4 --- /dev/null +++ b/docusaurus/docs/reactnative/hooks/poll/use-poll-state.mdx @@ -0,0 +1,140 @@ +--- +id: use-poll-state +title: usePollState +--- + +This is the hook containing most of the important properties from the `PollState`. It also provides several other utilities that make it easier to handle the UI for polls. + +The full extent of the `Poll` state can be found [here](https://github.com/GetStream/stream-chat-js/blob/master/src/poll.ts), under the `PollState` type. If the properties from the hook are not enough for rendering their desired UI, integrators are encouraged to write their own `selectors` and use the [`usePollStateStore`](./use-poll-state-store.mdx) hook instead. + +This hook can only be used within a child of [`PollContext`](../../contexts/poll-context.mdx). + +It is highly recommended that the user familiarizes themselves with the [`Poll` client documentation](https://getstream.io/chat/docs/javascript/polls_api/?language=javascript&q=poll) in order to understand the properties better. + +## Return type + +The `usePollState` hook returns an object containing values from the `PollState`. + +### `allow_answers` + +A property depicting whether answers (comments) are allowed to the `poll`. + +| Type | +| --------- | ----------- | +| `boolean` | `undefined` | + +### `allow_user_suggested_options` + +A property depicting whether user option suggestions are allowed to the `poll`. + +| Type | +| --------- | ----------- | +| `boolean` | `undefined` | + +### `answers_count` + +A property containing the number of answers (comments) to the `poll`. + +| Type | +| -------- | +| `number` | + +### `created_by` + +A property containing information about the user who created the `poll`. Its type can be found [here](https://github.com/GetStream/stream-chat-js/blob/b447512922b19bc7e3668bd9df81debcb673dd81/src/types.ts) under `UserResponse`. + +This property will be `null` for anonymous polls. + +| Type | +| -------- | ------ | +| `object` | `null` | + +### `enforce_unique_vote` + +A property depicting whether each user should have only one and unique vote or they would be able to vote multiple times during the lifespan of a `poll`. + +| Type | +| --------- | +| `boolean` | + +### `is_closed` + +A property depicting whether the `poll` is still open for voting or not. + +| Type | +| --------- | +| `boolean` | + +### `latest_votes_by_option` + +A property containing the latest votes for each option stored in an object by option ID. Only the last 10 votes are maintained here. If all votes are needed for an option, the [`usePollOptionVotesPagination`](./use-poll-option-votes-pagination.mdx) hook should be used instead. + +This property is not available for anonymous polls. + +| Type | +| -------------------------- | +| `Record` | + +### `max_votes_allowed` + +A property containing the maximum number of votes allowed per user. + +| Type | +| -------- | +| `number` | + +### `maxVotedOptionIds` + +An array of `Poll` option IDs of all of the options with the maximum number of votes in it. Will only contain more than 1 ID if there's a tie between options. + +| Type | +| ---------- | +| `string[]` | + +### `name` + +A property depicting the name of the `poll`. + +| Type | +| -------- | +| `string` | + +### `options` + +A property that is a list of all of the options a `poll` has. A `poll` cannot have more than 100 options. + +| Type | +| -------------- | +| `PollOption[]` | + +### `ownAnswer` + +A property containing the answer (comment) of a user to the current `poll`. Only one answer can be provided by a single user. This property will not be present in anonymous polls. + +| Type | +| ------------ | ----------- | +| `PollAnswer` | `undefined` | + +### `ownVotesByOptionId` + +A property containing the current user's own votes in a given `poll`, stored in an object by option ID. + +| Type | +| -------------------------- | +| `Record` | + +### `vote_counts_by_option` + +A property containing the vote counts in a given `poll`, stored in an object by option ID. + +| Type | +| ------------------------ | +| `Record` | + +### `voting_visibility` + +A property depicting the visibility of votes in a given `poll`. It conforms to the `VotingVisibility` enum found [here](https://github.com/GetStream/stream-chat-js/blob/b447512922b19bc7e3668bd9df81debcb673dd81/src/types.ts). + +| Type | +| ------------------ | ----------- | +| `VotingVisibility` | `undefined` | diff --git a/docusaurus/docs/reactnative/state-and-offline-support/state-overview.mdx b/docusaurus/docs/reactnative/state-and-offline-support/state-overview.mdx index 51474fc6f3..0e1c119758 100644 --- a/docusaurus/docs/reactnative/state-and-offline-support/state-overview.mdx +++ b/docusaurus/docs/reactnative/state-and-offline-support/state-overview.mdx @@ -248,12 +248,12 @@ To disconnect a user you can call `disconnect` on the client. await client.disconnectUser(); ``` -## Thread and ThreadManager +## POJO -With the new [threads feature](../guides/custom-thread-list.mdx) we've decided to refresh our state management and moved to a subscribable POJO with selector based system to make developer experience better when it came to rendering information provided by our `StreamChat` client. +With a few of our new features, we've decided to refresh our state management and moved to a subscribable POJO with selector based system to make developer experience better when it came to rendering information provided by our `StreamChat` client. :::note -This change is currently only available within `StreamChat.threads` but will be reused across the whole SDK later on. +This change is currently only available within `StreamChat.threads`, `StreamChat.poll` and `StreamChat.polls` but will be reused across the whole SDK later on. ::: ### Why POJO (State Object) @@ -349,7 +349,7 @@ thread?.state.getLatestValue(/*...*/); #### useStateStore Hook -For the ease of use - the React SDK comes with the appropriate state access hook which wraps `StateStore.subscribeWithSelector` API for the React-based applications. +For the ease of use - the React Native SDK comes with the appropriate state access hook which wraps `StateStore.subscribeWithSelector` API for the React-based applications. ```tsx import { useStateStore } from 'stream-chat-react-native'; @@ -370,3 +370,65 @@ const CustomThreadList = () => { ); }; ``` + +## Thread and ThreadManager + +One of the feature that follows the new POJO style of state management is the [threads feature](../guides/custom-thread-list.mdx). + +It provides a reactive state for both a single `Thread` (through a reactive `threadInstance`) and a list of threads (through `StreamChat.threads`). + +Both states can be accessed through `selector`s as outlined in the examples above. + +## Poll and PollManager + +Our new polls feature also follows the new POJO style of state management. A `poll` in itself is something that needs to be linked to a `message` in order for it to work. When a poll is created, the only way to make it visible to the users is to send it as a message. This `message` needs to have a `poll_id` attached to it and preferably no text. + +You can access each `poll`'s reactive state by getting it by ID using `StreamChat.polls.fromState()`. + +:::note +Please keep in mind that `message.poll` is not going to be reactive, but will rather contain the raw `poll` data as returned by our backend. +::: + +### Utility hooks + +The React Native SDK provides 2 utility hooks to help with consuming the `poll` state. They can be found listed below: + +- [`usePollStateStore`](./hooks/poll/use-poll-state-store.mdx) +- [`usePollState`](./hooks/poll/use-poll-state.mdx) + +Similarly to the `threads` feature, one can also directly use `useStateStore` and access `StreamChat.polls.fromState().state` through custom `selector`s. + +:::note +Both `usePollStateStore` and `usePollState` can only be used in children of a [`PollContext`](./contexts/poll-context.mdx). This impediment does not exist however on `useStateStore`. +::: + +Due to this, all `poll` related components within the SDK are self-wrapped within a `PollContext` and require `message` and `poll` as mandatory props. + +#### Example + +```tsx +import { usePollState } from 'stream-chat-react-native'; + +const CustomPollComponent = () => { + const { name, options } = usePollState(); + + return ( + + {name} + {options.map(option => ( + {option.text} + ))} + + ); +}; + +const PollMessage = ({ message }) => { + const { client } = useChatContext(); + const pollInstance = client.polls.fromState(message?.poll_id); + return ( + + + + ); +}; +``` diff --git a/docusaurus/docs/reactnative/ui-components/create-poll.mdx b/docusaurus/docs/reactnative/ui-components/create-poll.mdx new file mode 100644 index 0000000000..cc59771083 --- /dev/null +++ b/docusaurus/docs/reactnative/ui-components/create-poll.mdx @@ -0,0 +1,49 @@ +--- +id: create-poll +title: CreatePoll +--- + +import ClosePollCreationDialog from '../common-content/contexts/create-poll-context/close-poll-creation-dialog.mdx'; +import CreatePollContent from '../common-content/contexts/create-poll-context/create-poll-content.mdx'; +import CreatePollOptionHeight from '../common-content/contexts/create-poll-context/create-poll-option-height.mdx'; +import SendMessage from '../common-content/contexts/message-input-context/send_message.mdx'; + +A component used to render the entirety of the poll creation form. It encapsulates the `CreatePollContent` component within a [`CreatePollContext`](../contexts/create-poll-context.mdx). Needs to be structured inside a [`Channel` component](../core-components/channel). + +The default behaviour of this component is it opening in a [React Native Modal](https://reactnative.dev/docs/modal). Integrators are highly encouraged to write their own UI for the dialog with the specific options and flexibility they require. + +## General Usage + +```tsx +import { OverlayProvider, Chat, Channel, CreatePoll } from 'stream-chat-react-native'; + +const App = () => { + return ( + + + + + + + + ); +}; +``` + +## Props + +###
_forwarded from [MessageInputContext](../../contexts/message-input-context#sendmessage)_ props
sendMessage {#sendmessage} + + + +### `closePollCreationDialog` + + + +### `onThreadSelect` + + + +### `CreatePollContent` + + diff --git a/docusaurus/docs/reactnative/ui-components/poll-all-options.mdx b/docusaurus/docs/reactnative/ui-components/poll-all-options.mdx new file mode 100644 index 0000000000..2bfdeb0e60 --- /dev/null +++ b/docusaurus/docs/reactnative/ui-components/poll-all-options.mdx @@ -0,0 +1,87 @@ +--- +id: poll-all-options +title: PollAllOptions +--- + +import MessageProp from '../common-content/contexts/message-context/message.mdx'; +import Poll from '../common-content/contexts/poll-context/poll.mdx'; + +A component used to render a list of all of the options that a `Poll` has. Voting on each option on this screen is also enabled. Needs to be structured inside a [`Channel` component](../core-components/channel). + +It will render its default `PollAllOptionsContent`, which can be overridden for custom UI. Its children have access to the entire `poll` state through the `usePollState` hook. + +## General Usage + +```tsx +import { OverlayProvider, Chat, Channel, PollAllOptions } from 'stream-chat-react-native'; + +const App = () => { + return ( + + + + + + + + ); +}; +``` + +## Props + +###
_forwarded from [MessageContext](../../contexts/message-context#message)_ props
message {#message} + + + +### `poll` \* + + + +### `additionalScrollViewProps` + +A prop used to override the underlying [`ScrollView`](https://reactnative.dev/docs/scrollview#props) props of the `PollAllOptions`. + +```jsx +const flatListProps = { bounces: true }; + +; +``` + +| Type | +| ------ | +| object | + +### `PollAllOptionsContent` + +A `Component` prop used to render the content of the `PollAllOptions` component. + +The component has full access to the entire `Poll` reactive state through the `usePollState` hook. + +| Type | Default | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`PollAllOptionsContent`](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Poll/PollOption.tsx) | + +#### Usage + +```tsx +import { Text } from 'react-native'; +import { OverlayProvider, Chat, Channel, PollAllOptions, usePollState } from 'stream-chat-react-native'; + +const MyPollAllOptionsContent = () => { + const { options } = usePollState(); + return options.map(option => {option.id}); +}; + +const App = () => { + return ( + + + + + + + + ); +}; +``` diff --git a/docusaurus/docs/reactnative/ui-components/poll-answers-list.mdx b/docusaurus/docs/reactnative/ui-components/poll-answers-list.mdx new file mode 100644 index 0000000000..6c0dc4a9c1 --- /dev/null +++ b/docusaurus/docs/reactnative/ui-components/poll-answers-list.mdx @@ -0,0 +1,104 @@ +--- +id: poll-answers-list +title: PollAnswersList +--- + +import MessageProp from '../common-content/contexts/message-context/message.mdx'; +import Poll from '../common-content/contexts/poll-context/poll.mdx'; + +A component used to render a list of answers that a `Poll` has. The results will be paginated and only one answer per user is allowed. Needs to be structured inside a [`Channel` component](../core-components/channel). + +It will render its default `PollAnswersListContent`, which can be overridden for custom UI. Its children have access to the entire `poll` state through the `usePollState` hook. + +To access the paginated list of answers, the `usePollAnswersPagination` hook can be used. + +## General Usage + +```tsx +import { OverlayProvider, Chat, Channel, PollAnswersList } from 'stream-chat-react-native'; + +const App = () => { + return ( + + + + + + + + ); +}; +``` + +## Props + +###
_forwarded from [MessageContext](../../contexts/message-context#message)_ props
message {#message} + + + +### `poll` \* + + + +### `additionalFlatListProps` + +A prop used to override the underlying [`FlatList`](https://reactnative.dev/docs/flatlist#props) props of the `PollAnswersList`. + +```jsx +const flatListProps = { bounces: true }; + +; +``` + +| Type | +| ------ | +| object | + +### `PollAnswersListContent` + +A `Component` prop used to render the content of the `PollAnswersList` component. + +The component has full access to the entire `Poll` reactive state through the `usePollState` hook. + +| Type | Default | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`PollAnswersListContent`](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Poll/PollAnswersList.tsx) | + +#### Usage + +```tsx +import { Text, FlatList } from 'react-native'; +import { + OverlayProvider, + Chat, + Channel, + PollAnswersList, + usePollAnswersPagination, + usePollState, +} from 'stream-chat-react-native'; + +// will only display the first page of answers without loading more +const MyPollAnswersListContent = () => { + const { name } = usePollState(); + const { pollAnswers } = usePollAnswersPagination(); + return ( + {name}} + data={pollAnswers} + renderItem={({ item }) => {item.answer_text}} + /> + ); +}; + +const App = () => { + return ( + + + + + + + + ); +}; +``` diff --git a/docusaurus/docs/reactnative/ui-components/poll-buttons.mdx b/docusaurus/docs/reactnative/ui-components/poll-buttons.mdx new file mode 100644 index 0000000000..b022d2f3a0 --- /dev/null +++ b/docusaurus/docs/reactnative/ui-components/poll-buttons.mdx @@ -0,0 +1,258 @@ +--- +id: poll-buttons +title: Poll Buttons +--- + +For the purposes of managing the `Poll` UI more easily, we expose various button components that have an overridable `onPress` property in case the default behaviour needs to be changed. + +The `onPress` property should be typically used to navigate to the various `Poll` screens using a popular navigation library rather than relying on the default [React Native Modal](https://reactnative.dev/docs/modal) implementation. Integrators are encouraged to handle navigation to these screens like this rather than relying on the defaults. + +In order to be able to render the screens, all of the required props will be passed to the `onPress` method. + +Conversely, if one wishes to use the default button behaviour they may render them without any props. + +All of the buttons need to be structured inside a [`Channel` component](../core-components/channel). + +## `GenericPollButton` + +A generic button component that has the default UI encapsulated within it. + +### Props + +#### `title` \* + +The text that is going to be rendered as the button's content. + +| Type | +| ------ | +| string | + +#### `onPress` \* + +A press handler that will be invoked whenever the button is pressed. + +| Type | +| ------------ | +| `() => void` | + +### Example usage + +```tsx +import { GenericPollButton } from 'stream-chat-react-native'; + +const button = () => console.log('I got pressed !')} />; +``` + +## `ViewResultsButton` + +A button responsible for opening the `PollResults` modal. + +### Props + +#### `onPress` + +A press handler that will be invoked whenever the button is pressed. + +| Type | +| ----------------------------- | +| `({ message, poll }) => void` | + +### Example usage + +```tsx +import { ViewResultsButton } from 'stream-chat-react-native'; + +const button = () => ( + console.log(`Poll ID: ${poll.id}; Message ID: ${message.id}`)} /> +); +``` + +## `EndVoteButton` + +A button responsible for closing voting on the `Poll`. + +### Example usage + +```tsx +import { EndVoteButton } from 'stream-chat-react-native'; + +const button = () => ; +``` + +## `AddCommentButton` + +A button responsible for opening the `Poll` input dialog used to add a new comment to it. + +### Props + +#### `onPress` + +A press handler that will be invoked whenever the button is pressed. + +| Type | +| ----------------------------- | +| `({ message, poll }) => void` | + +### Example usage + +```tsx +import { AddCommentButton } from 'stream-chat-react-native'; + +const button = () => ( + console.log(`Poll ID: ${poll.id}; Message ID: ${message.id}`)} /> +); +``` + +## `ShowAllCommentsButton` + +A button responsible for opening the `PollAnswersList` modal. + +### Props + +#### `onPress` + +A press handler that will be invoked whenever the button is pressed. + +| Type | +| ----------------------------- | +| `({ message, poll }) => void` | + +### Example usage + +```tsx +import { ShowAllCommentsButton } from 'stream-chat-react-native'; + +const button = () => ( + console.log(`Poll ID: ${poll.id}; Message ID: ${message.id}`)} + /> +); +``` + +## `AnswerListAddCommentButton` + +A button responsible for opening the `Poll` input dialog used to add a new comment to it, from within `PollAnswersList`. + +It has the same props and usage as [`AddCommentButton`](#addcommentbutton). + +## `SuggestOptionButton` + +A button responsible for opening the `Poll` input dialog used to suggest new options to the it. + +### Props + +#### `onPress` + +A press handler that will be invoked whenever the button is pressed. + +| Type | +| ----------------------------- | +| `({ message, poll }) => void` | + +### Example usage + +```tsx +import { SuggestOptionButton } from 'stream-chat-react-native'; + +const button = () => ( + console.log(`Poll ID: ${poll.id}; Message ID: ${message.id}`)} /> +); +``` + +## `ShowAllOptionsButton` + +A button responsible for opening the `PollAllOptions` modal. + +### Props + +#### `onPress` + +A press handler that will be invoked whenever the button is pressed. + +| Type | +| ----------------------------- | +| `({ message, poll }) => void` | + +### Example usage + +```tsx +import { ShowAllOptionsButton } from 'stream-chat-react-native'; + +const button = () => ( + console.log(`Poll ID: ${poll.id}; Message ID: ${message.id}`)} + /> +); +``` + +## `VoteButton` + +A button responsible for voting on a specific option within a `Poll`. + +### Props + +#### `option` \* + +The `poll` option that we want to vote on. + +| Type | +| ------ | +| object | + +#### `onPress` + +A press handler that will be invoked whenever the button is pressed. + +| Type | +| ----------------------------- | +| `({ message, poll }) => void` | + +### Example usage + +```tsx +import { VoteButton } from 'stream-chat-react-native'; + +const button = () => ( + console.log(`Poll ID: ${poll.id}; Message ID: ${message.id}`)} + /> +); +``` + +## `ShowAllVotesButton` + +A button responsible for opening the `PollOptionFullResults` modal. + +### Props + +#### `option` \* + +The `poll` option that we want to vote on. + +| Type | +| ------ | +| object | + +#### `onPress` + +A press handler that will be invoked whenever the button is pressed. + +| Type | +| ------------------------------------- | +| `({ message, option, poll }) => void` | + +### Example usage + +```tsx +import { ShowAllVotesButton } from 'stream-chat-react-native'; + +const button = () => ( + + console.log(`Poll ID: ${poll.id}; Message ID: ${message.id}; Option ID: ${option.id}`) + } + /> +); +``` diff --git a/docusaurus/docs/reactnative/ui-components/poll-option-full-results.mdx b/docusaurus/docs/reactnative/ui-components/poll-option-full-results.mdx new file mode 100644 index 0000000000..a0a389230e --- /dev/null +++ b/docusaurus/docs/reactnative/ui-components/poll-option-full-results.mdx @@ -0,0 +1,117 @@ +--- +id: poll-option-full-results +title: PollOptionFullResults +--- + +import MessageProp from '../common-content/contexts/message-context/message.mdx'; +import Poll from '../common-content/contexts/poll-context/poll.mdx'; + +A component all of the votes for a specific `Poll` option. The results will be paginated and updated as they change. Needs to be structured inside a [`Channel` component](../core-components/channel). + +It will render its default `PollOptionFullResultsContent`, which can be overridden for custom UI. Its children have access to the entire `poll` state through the `usePollState` hook. + +To access the paginated list of votes, the `usePollOptionVotesPagination` hook can be used. + +## General Usage + +```tsx +import { OverlayProvider, Chat, Channel, PollOptionFullResults } from 'stream-chat-react-native'; + +const App = () => { + return ( + + + + + + + + ); +}; +``` + +## Props + +###
_forwarded from [MessageContext](../../contexts/message-context#message)_ props
message {#message} + + + +### `poll` \* + + + +### `option` \* + +The `poll` option that we want to display the results for. + +| Type | +| ------ | +| object | + +### `additionalFlatListProps` + +A prop used to override the underlying [`FlatList`](https://reactnative.dev/docs/flatlist#props) props of the `PollOptionFullResults`. + +```jsx +const flatListProps = { bounces: true }; + +; +``` + +| Type | +| ------ | +| object | + +### `PollOptionFullResultsContent` + +A `Component` prop used to render the content of the `PollOptionFullResults` component. + +The component has full access to the entire `Poll` reactive state through the `usePollState` hook. + +| Type | Default | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`PollOptionFullResultsContent`](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Poll/PollOptionFullResults.tsx) | + +#### Usage + +```tsx +import { Text, FlatList } from 'react-native'; +import { + OverlayProvider, + Chat, + Channel, + PollOptionFullResults, + usePollOptionVotesPagination, + usePollState, +} from 'stream-chat-react-native'; + +// will only display the first page of votes without loading more +const MyPollOptionFullResultsContent = ({ option }) => { + const { name } = usePollState(); + const { votes } = usePollOptionVotesPagination({ option }); + return ( + {name}} + data={votes} + renderItem={({ item }) => {item.id}} + /> + ); +}; + +const App = () => { + return ( + + + + + + + + ); +}; +``` diff --git a/docusaurus/docs/reactnative/ui-components/poll-results.mdx b/docusaurus/docs/reactnative/ui-components/poll-results.mdx new file mode 100644 index 0000000000..370c519e06 --- /dev/null +++ b/docusaurus/docs/reactnative/ui-components/poll-results.mdx @@ -0,0 +1,87 @@ +--- +id: poll-results +title: PollResults +--- + +import MessageProp from '../common-content/contexts/message-context/message.mdx'; +import Poll from '../common-content/contexts/poll-context/poll.mdx'; + +A component used to render a list of all of the votes per option that a `Poll` has. Each option will display the 5 most recent votes. If an option has more than 5 votes it will add a button at the bottom that opens the [`PollOptionFullResults`](./poll-option-full-results.mdx) Modal. Needs to be structured inside a [`Channel` component](../core-components/channel). + +It will render its default `PollResultsContent`, which can be overridden for custom UI. Its children have access to the entire `poll` state through the `usePollState` hook. + +## General Usage + +```tsx +import { OverlayProvider, Chat, Channel, PollResults } from 'stream-chat-react-native'; + +const App = () => { + return ( + + + + + + + + ); +}; +``` + +## Props + +###
_forwarded from [MessageContext](../../contexts/message-context#message)_ props
message {#message} + + + +### `poll` \* + + + +### `additionalScrollViewProps` + +A prop used to override the underlying [`ScrollView`](https://reactnative.dev/docs/scrollview#props) props of the `PollResults`. + +```jsx +const flatListProps = { bounces: true }; + +; +``` + +| Type | +| ------ | +| object | + +### `PollResultsContent` + +A `Component` prop used to render the content of the `PollResults` component. + +The component has full access to the entire `Poll` reactive state through the `usePollState` hook. + +| Type | Default | +| ------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| ComponentType | [`PollResultsContent`](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/Poll/PollResults.tsx) | + +#### Usage + +```tsx +import { Text } from 'react-native'; +import { OverlayProvider, Chat, Channel, PollResults, usePollState } from 'stream-chat-react-native'; + +const MyPollResultsContent = () => { + const { options } = usePollState(); + return options.map(option => {option.id}); +}; + +const App = () => { + return ( + + + + + + + + ); +}; +``` diff --git a/docusaurus/docs/reactnative/ui-components/poll.mdx b/docusaurus/docs/reactnative/ui-components/poll.mdx new file mode 100644 index 0000000000..fa26908ef5 --- /dev/null +++ b/docusaurus/docs/reactnative/ui-components/poll.mdx @@ -0,0 +1,26 @@ +--- +id: poll +title: Poll +--- + +import MessageProp from '../common-content/contexts/message-context/message.mdx'; +import Poll from '../common-content/contexts/poll-context/poll.mdx'; +import PollContent from '../common-content/ui-components/poll/poll-content.mdx'; + +A component used to render a poll within a `MessageList`. Needs to be structured inside a [`Channel` component](../core-components/channel). + +It will render its default `PollContent`, which can be overridden for custom UI. Its children have access to the entire `poll` state through the `usePollState` hook. + +## Props + +###
_forwarded from [MessageContext](../../contexts/message-context#message)_ props
message {#message} + + + +### `poll` \* + + + +### `PollContent` + + diff --git a/docusaurus/sidebars-react-native.json b/docusaurus/sidebars-react-native.json index 7e2bff155e..e4df12e1b4 100644 --- a/docusaurus/sidebars-react-native.json +++ b/docusaurus/sidebars-react-native.json @@ -98,6 +98,27 @@ { "Thread List": ["ui-components/thread-list", "ui-components/thread-list-item"] }, + { + "Poll": [ + "ui-components/create-poll", + "ui-components/poll", + "ui-components/poll-all-options", + "ui-components/poll-answers-list", + "ui-components/poll-results", + "ui-components/poll-option-full-results", + "ui-components/poll-buttons", + { + "Hooks": [ + "hooks/poll/use-poll-context", + "hooks/poll/use-create-poll-context", + "hooks/poll/use-poll-state-store", + "hooks/poll/use-poll-state", + "hooks/poll/use-poll-answers-pagination", + "hooks/poll/use-poll-option-votes-pagination" + ] + } + ] + }, { "Contexts": [ "customization/contexts", @@ -112,7 +133,9 @@ "contexts/translation-context", "contexts/typing-context", "contexts/threads-context", - "contexts/thread-list-item-context" + "contexts/thread-list-item-context", + "contexts/create-poll-context", + "contexts/poll-context" ] } ], @@ -129,7 +152,8 @@ "guides/file-compression", "guides/blocking-users", "guides/live-location-sharing", - "guides/custom-thread-list" + "guides/custom-thread-list", + "guides/custom-poll-flow" ], "State and Offline Support": [ "state-and-offline-support/state-overview", diff --git a/examples/ExpoMessaging/yarn.lock b/examples/ExpoMessaging/yarn.lock index f671c3845f..48fd048ebd 100644 --- a/examples/ExpoMessaging/yarn.lock +++ b/examples/ExpoMessaging/yarn.lock @@ -7310,10 +7310,10 @@ stream-buffers@2.2.x, stream-buffers@~2.2.0: version "0.0.0" uid "" -stream-chat-react-native-core@5.39.0: - version "5.39.0" - resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.39.0.tgz#499db88ff9a6ef2f93012f2dd1c0feb3c849859c" - integrity sha512-dzA/N3pOWMmRtNmMWYfnOnGSBG+K+ReKqLL3CXYmSnUmEHQlJwHqKDLPlxnNY8IOT2Nh067m04aB+1OQZpi/Mw== +stream-chat-react-native-core@5.39.6: + version "5.39.6" + resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.39.6.tgz#bc40925070a55e45afb98d6518c899ab80273486" + integrity sha512-DlPC4UzBa/aP7UeT/e0axJ/S4y3QQ1TS6oxrYVQlZGasFxgmL+OUDfoj6HwX9J/DOSrogw9/YF6yZLXTorlB/g== dependencies: "@gorhom/bottom-sheet" "^4.6.4" dayjs "1.10.5" @@ -7326,16 +7326,16 @@ stream-chat-react-native-core@5.39.0: path "0.12.7" react-native-markdown-package "1.8.2" react-native-url-polyfill "^1.3.0" - stream-chat "8.40.8" + stream-chat "8.41.1" "stream-chat-react-native-core@link:../../package": version "0.0.0" uid "" -stream-chat@8.40.8: - version "8.40.8" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.40.8.tgz#0f5320bd8b03d1cbff377f8c7ae2f8afe24d0515" - integrity sha512-nYLvYAkrvXRzuPO52TIofNiInCkDdXrnBc/658297lC6hzrHNc87mmTht264BXmXLlpasTNP3rLKxR6MxhpgKg== +stream-chat@8.41.1: + version "8.41.1" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.41.1.tgz#c991980b800b67ec38202a1aa3bbbd4112ccb5fa" + integrity sha512-WV0mHHm88D4RbAV42sD0+SqTWLCvjIwfGZ3nSBXRAuGpVYJEqnNUhEd4OIQ+YrXVbjY7qWz9L5XRk5fZIfE9kg== dependencies: "@babel/runtime" "^7.16.3" "@types/jsonwebtoken" "~9.0.0" diff --git a/examples/SampleApp/ios/SampleApp.xcodeproj/project.pbxproj b/examples/SampleApp/ios/SampleApp.xcodeproj/project.pbxproj index 1626938708..fdbe27f968 100644 --- a/examples/SampleApp/ios/SampleApp.xcodeproj/project.pbxproj +++ b/examples/SampleApp/ios/SampleApp.xcodeproj/project.pbxproj @@ -513,8 +513,12 @@ PRODUCT_BUNDLE_IDENTIFIER = io.getstream.reactnative.SampleApp; PRODUCT_NAME = SampleApp; PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -544,7 +548,11 @@ PRODUCT_BUNDLE_IDENTIFIER = io.getstream.reactnative.SampleApp; PRODUCT_NAME = SampleApp; PROVISIONING_PROFILE_SPECIFIER = "match AdHoc io.getstream.reactnative.SampleApp"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -606,57 +614,57 @@ "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", ); IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = ( @@ -678,10 +686,7 @@ "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; @@ -741,57 +746,57 @@ "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon-Samples/ReactCommon_Samples.framework/Headers/platform/ios", "${PODS_CONFIGURATION_BUILD_DIR}/React-NativeModulesApple/React_NativeModulesApple.framework/Headers", "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers/react/renderer/graphics/platform/ios", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", - " ${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/ReactCommon/ReactCommon.framework/Headers", + "${PODS_CONFIGURATION_BUILD_DIR}/React-Fabric/React_Fabric.framework/Headers/react/renderer/components/view/platform/cxx", + "${PODS_CONFIGURATION_BUILD_DIR}/React-graphics/React_graphics.framework/Headers", ); IPHONEOS_DEPLOYMENT_TARGET = 13.4; LD_RUNPATH_SEARCH_PATHS = ( @@ -812,10 +817,7 @@ "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", ); - OTHER_LDFLAGS = ( - "$(inherited)", - " ", - ); + OTHER_LDFLAGS = "$(inherited) "; REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; USE_HERMES = true; diff --git a/examples/SampleApp/src/components/UnreadCountBadge.tsx b/examples/SampleApp/src/components/UnreadCountBadge.tsx index 8ed279f6a7..b457b6790c 100644 --- a/examples/SampleApp/src/components/UnreadCountBadge.tsx +++ b/examples/SampleApp/src/components/UnreadCountBadge.tsx @@ -20,11 +20,12 @@ const styles = StyleSheet.create({ }, }); -const selector = (nextValue: ThreadManagerState) => [nextValue.unreadThreadCount]; +const selector = (nextValue: ThreadManagerState) => + ({ unreadCount: nextValue.unreadThreadCount } as const); export const ThreadsUnreadCountBadge: React.FC = () => { const { chatClient } = useAppContext(); - const [unreadCount] = useStateStore(chatClient?.threads?.state, selector); + const { unreadCount } = useStateStore(chatClient?.threads?.state, selector); return ; }; diff --git a/examples/SampleApp/src/hooks/useStreamChatTheme.ts b/examples/SampleApp/src/hooks/useStreamChatTheme.ts index 8b9882a14c..e241ccd2b5 100644 --- a/examples/SampleApp/src/hooks/useStreamChatTheme.ts +++ b/examples/SampleApp/src/hooks/useStreamChatTheme.ts @@ -11,6 +11,7 @@ const getChatStyle = (colorScheme: ColorSchemeName): DeepPartial => ({ accent_red: '#FF3742', bg_gradient_end: '#101214', bg_gradient_start: '#070A0D', + bg_user: '#17191C', black: '#FFFFFF', blue_alice: '#00193D', border: '#141924', @@ -57,6 +58,16 @@ const getChatStyle = (colorScheme: ColorSchemeName): DeepPartial => ({ white_smoke: '#F2F2F2', white_snow: '#FCFCFC', }, + ...(colorScheme === 'dark' + ? { + messageSimple: { + content: { + receiverMessageBackgroundColor: '#2D2F2F', + senderMessageBackgroundColor: '#101418', + }, + }, + } + : {}), }); export const useStreamChatTheme = () => { diff --git a/examples/SampleApp/src/screens/ThreadScreen.tsx b/examples/SampleApp/src/screens/ThreadScreen.tsx index e132d6c81a..a138922a46 100644 --- a/examples/SampleApp/src/screens/ThreadScreen.tsx +++ b/examples/SampleApp/src/screens/ThreadScreen.tsx @@ -18,7 +18,7 @@ import type { RouteProp } from '@react-navigation/native'; import type { StackNavigatorParamList, StreamChatGenerics } from '../types'; import { ThreadState } from 'stream-chat'; -const selector = (nextValue: ThreadState) => [nextValue.parentMessage] as const; +const selector = (nextValue: ThreadState) => ({ parentMessage: nextValue.parentMessage } as const); const styles = StyleSheet.create({ container: { @@ -39,7 +39,8 @@ export type ThreadHeaderProps = { const ThreadHeader: React.FC = ({ thread }) => { const typing = useTypingString(); let subtitleText = thread?.user?.name; - const [parentMessage] = useStateStore(thread?.threadInstance?.state ?? undefined, selector) || []; + const { parentMessage } = + useStateStore(thread?.threadInstance?.state ?? undefined, selector) || []; if (subtitleText == null) { subtitleText = parentMessage?.user?.name; diff --git a/examples/SampleApp/yarn.lock b/examples/SampleApp/yarn.lock index 2183e6da58..a8f10687db 100644 --- a/examples/SampleApp/yarn.lock +++ b/examples/SampleApp/yarn.lock @@ -6837,10 +6837,10 @@ statuses@~1.5.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -stream-chat-react-native-core@5.39.0: - version "5.39.0" - resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.39.0.tgz#499db88ff9a6ef2f93012f2dd1c0feb3c849859c" - integrity sha512-dzA/N3pOWMmRtNmMWYfnOnGSBG+K+ReKqLL3CXYmSnUmEHQlJwHqKDLPlxnNY8IOT2Nh067m04aB+1OQZpi/Mw== +stream-chat-react-native-core@5.39.6: + version "5.39.6" + resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.39.6.tgz#bc40925070a55e45afb98d6518c899ab80273486" + integrity sha512-DlPC4UzBa/aP7UeT/e0axJ/S4y3QQ1TS6oxrYVQlZGasFxgmL+OUDfoj6HwX9J/DOSrogw9/YF6yZLXTorlB/g== dependencies: "@gorhom/bottom-sheet" "^4.6.4" dayjs "1.10.5" @@ -6853,7 +6853,7 @@ stream-chat-react-native-core@5.39.0: path "0.12.7" react-native-markdown-package "1.8.2" react-native-url-polyfill "^1.3.0" - stream-chat "8.40.8" + stream-chat "8.41.1" "stream-chat-react-native-core@link:../../package": version "0.0.0" @@ -6863,10 +6863,25 @@ stream-chat-react-native-core@5.39.0: version "0.0.0" uid "" -stream-chat@8.40.8: - version "8.40.8" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.40.8.tgz#0f5320bd8b03d1cbff377f8c7ae2f8afe24d0515" - integrity sha512-nYLvYAkrvXRzuPO52TIofNiInCkDdXrnBc/658297lC6hzrHNc87mmTht264BXmXLlpasTNP3rLKxR6MxhpgKg== +stream-chat@8.41.1: + version "8.41.1" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.41.1.tgz#c991980b800b67ec38202a1aa3bbbd4112ccb5fa" + integrity sha512-WV0mHHm88D4RbAV42sD0+SqTWLCvjIwfGZ3nSBXRAuGpVYJEqnNUhEd4OIQ+YrXVbjY7qWz9L5XRk5fZIfE9kg== + dependencies: + "@babel/runtime" "^7.16.3" + "@types/jsonwebtoken" "~9.0.0" + "@types/ws" "^7.4.0" + axios "^1.6.0" + base64-js "^1.5.1" + form-data "^4.0.0" + isomorphic-ws "^4.0.1" + jsonwebtoken "~9.0.0" + ws "^7.5.10" + +stream-chat@8.42.0: + version "8.42.0" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.42.0.tgz#124ea2c10c6e8f7990304e1101c66751daf63e6c" + integrity sha512-8xZz+fmdHSOa3L1rHUOC4Wah+ipvLvdiOmeOfGK6uXnLOKlSHMOblwmQErrOoFM4SKfX9Bea3V8viaKUu6bPng== dependencies: "@babel/runtime" "^7.16.3" "@types/jsonwebtoken" "~9.0.0" diff --git a/examples/TypeScriptMessaging/yarn.lock b/examples/TypeScriptMessaging/yarn.lock index fedc05320c..f5f4802635 100644 --- a/examples/TypeScriptMessaging/yarn.lock +++ b/examples/TypeScriptMessaging/yarn.lock @@ -6899,10 +6899,10 @@ statuses@~1.5.0: resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== -stream-chat-react-native-core@5.38.0: - version "5.38.0" - resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.38.0.tgz#3d394a8ea9ab8d9a06a47000ff90a44daae35aaa" - integrity sha512-l9wF3Y+caFL1J2urw+Od0U2JNkW4ff/o4rlpmIOe92Ow14kuQOrejKo1XJbC6riJBVpnrtT1tj8SfVeS2crW0Q== +stream-chat-react-native-core@5.39.6: + version "5.39.6" + resolved "https://registry.yarnpkg.com/stream-chat-react-native-core/-/stream-chat-react-native-core-5.39.6.tgz#bc40925070a55e45afb98d6518c899ab80273486" + integrity sha512-DlPC4UzBa/aP7UeT/e0axJ/S4y3QQ1TS6oxrYVQlZGasFxgmL+OUDfoj6HwX9J/DOSrogw9/YF6yZLXTorlB/g== dependencies: "@gorhom/bottom-sheet" "^4.6.4" dayjs "1.10.5" @@ -6915,7 +6915,7 @@ stream-chat-react-native-core@5.38.0: path "0.12.7" react-native-markdown-package "1.8.2" react-native-url-polyfill "^1.3.0" - stream-chat "8.39.0" + stream-chat "8.41.1" "stream-chat-react-native-core@link:../../package": version "0.0.0" @@ -6930,10 +6930,10 @@ stream-chat-react-native-devtools@^1.1.0: version "0.0.0" uid "" -stream-chat@8.39.0: - version "8.39.0" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.39.0.tgz#f4cb86bd5cac4c1272c24cd66ed4752bcda8d717" - integrity sha512-zQZR1tPrgGBbu+Gnv9F9KQx3OPUMvb0FN+39BEjkjgjRPm2JYhF78jfcYutQMiC538t3V+NgFGgj5N4sZvSsUA== +stream-chat@8.41.1: + version "8.41.1" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.41.1.tgz#c991980b800b67ec38202a1aa3bbbd4112ccb5fa" + integrity sha512-WV0mHHm88D4RbAV42sD0+SqTWLCvjIwfGZ3nSBXRAuGpVYJEqnNUhEd4OIQ+YrXVbjY7qWz9L5XRk5fZIfE9kg== dependencies: "@babel/runtime" "^7.16.3" "@types/jsonwebtoken" "~9.0.0" @@ -6943,7 +6943,7 @@ stream-chat@8.39.0: form-data "^4.0.0" isomorphic-ws "^4.0.1" jsonwebtoken "~9.0.0" - ws "^7.4.4" + ws "^7.5.10" strict-uri-encode@^2.0.0: version "2.0.0" @@ -7481,11 +7481,16 @@ ws@^6.2.2: dependencies: async-limiter "~1.0.0" -ws@^7, ws@^7.4.4, ws@^7.5.1: +ws@^7, ws@^7.5.1: version "7.5.9" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591" integrity sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q== +ws@^7.5.10: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== + xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" diff --git a/package/package.json b/package/package.json index 0c9bdd1199..4208f7f48c 100644 --- a/package/package.json +++ b/package/package.json @@ -78,7 +78,7 @@ "path": "0.12.7", "react-native-markdown-package": "1.8.2", "react-native-url-polyfill": "^1.3.0", - "stream-chat": "8.41.1" + "stream-chat": "8.42.0" }, "peerDependencies": { "react-native-quick-sqlite": ">=5.1.0", diff --git a/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx b/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx index 881d366906..5d26624378 100644 --- a/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx +++ b/package/src/components/AttachmentPicker/components/AttachmentPickerSelectionBar.tsx @@ -1,6 +1,11 @@ import React from 'react'; import { StyleSheet, TouchableOpacity, View } from 'react-native'; +import { + useChannelContext, + useMessagesContext, + useOwnCapabilitiesContext, +} from '../../../contexts'; import { useAttachmentPickerContext } from '../../../contexts/attachmentPickerContext/AttachmentPickerContext'; import { useMessageInputContext } from '../../../contexts/messageInputContext/MessageInputContext'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; @@ -21,14 +26,25 @@ export const AttachmentPickerSelectionBar = () => { attachmentSelectionBarHeight, CameraSelectorIcon, closePicker, + CreatePollIcon, FileSelectorIcon, ImageSelectorIcon, selectedPicker, setSelectedPicker, } = useAttachmentPickerContext(); - const { hasCameraPicker, hasFilePicker, imageUploads, pickFile, takeAndUploadImage } = - useMessageInputContext(); + const { + hasCameraPicker, + hasFilePicker, + imageUploads, + openPollCreationDialog, + pickFile, + sendMessage, + takeAndUploadImage, + } = useMessageInputContext(); + const { threadList } = useChannelContext(); + const { hasCreatePoll } = useMessagesContext(); + const ownCapabilities = useOwnCapabilitiesContext(); const { theme: { @@ -51,6 +67,12 @@ export const AttachmentPickerSelectionBar = () => { pickFile(); }; + const openPollCreationModal = () => { + setSelectedPicker(undefined); + closePicker(); + openPollCreationDialog?.({ sendMessage }); + }; + return ( { ) : null} + {!threadList && hasCreatePoll && ownCapabilities.sendPoll ? ( // do not allow poll creation in threads + + + + + + ) : null} ); }; diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 7acc9e3058..2266914d2e 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -208,6 +208,7 @@ const throttleOptions = { leading: true, trailing: true, }; + const debounceOptions = { leading: true, trailing: true, @@ -330,6 +331,8 @@ export type ChannelPropsWithContext< | 'TypingIndicatorContainer' | 'UrlPreview' | 'VideoThumbnail' + | 'PollContent' + | 'hasCreatePoll' > > & Partial, 'allowThreadMessagesInChannel'>> & { @@ -417,7 +420,7 @@ export type ChannelPropsWithContext< * Tells if channel is rendering a thread list */ threadList?: boolean; - }; + } & Partial>; const ChannelWithContext = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, @@ -459,6 +462,7 @@ const ChannelWithContext = < CommandsButton = CommandsButtonDefault, compressImageQuality, CooldownTimer = CooldownTimerDefault, + CreatePollContent, DateHeader = DateHeaderDefault, deletedMessagesVisibilityType = 'always', disableIfFrozenChannel = true, @@ -499,9 +503,10 @@ const ChannelWithContext = < handleReaction, handleRetry, handleThreadReply, + // If pickDocument isn't available, default to hiding the file picker hasCameraPicker = isImagePickerAvailable(), hasCommands = true, - // If pickDocument isn't available, default to hiding the file picker + hasCreatePoll, hasFilePicker = pickDocument !== null, hasImagePicker = true, hideDateSeparators = false, @@ -539,7 +544,7 @@ const ChannelWithContext = < MessageAvatar = MessageAvatarDefault, MessageBounce = MessageBounceDefault, MessageContent = MessageContentDefault, - messageContentOrder = ['quoted_reply', 'gallery', 'files', 'text', 'attachments'], + messageContentOrder = ['quoted_reply', 'gallery', 'files', 'poll', 'text', 'attachments'], MessageDeleted = MessageDeletedDefault, MessageEditedTimestamp = MessageEditedTimestampDefault, MessageError = MessageErrorDefault, @@ -565,8 +570,10 @@ const ChannelWithContext = < onLongPressMessage, onPressInMessage, onPressMessage, + openPollCreationDialog, OverlayReactionList = OverlayReactionListDefault, overrideOwnCapabilities, + PollContent, ReactionList = ReactionListDefault, read, Reply = ReplyDefault, @@ -652,6 +659,7 @@ const ChannelWithContext = < const uploadAbortControllerRef = useRef>(new Map()); const channelId = channel?.id || ''; + const pollCreationEnabled = !channel.disconnected && !!channel?.id && channel?.getConfig()?.polls; useEffect(() => { const initChannel = async () => { @@ -844,7 +852,13 @@ const ChannelWithContext = < setThreadMessages(updatedThreadMessages); } - if (channel && thread?.id && event.message?.id === thread.id && !threadInstance) { + if ( + channel && + thread?.id && + event.message?.id === thread.id && + !threadInstance && + !thread.poll_id + ) { const updatedThread = channel.state.formatMessage(event.message); setThread(updatedThread); } @@ -1548,6 +1562,8 @@ const ChannelWithContext = < attachments, mentioned_users, parent_id, + poll, + poll_id, text, ...extraFields }: Partial>) => { @@ -1569,6 +1585,8 @@ const ChannelWithContext = < id: userId, })) || [], parent_id, + poll, + poll_id, reactions: [], status: MessageStatusTypes.SENDING, text, @@ -2283,6 +2301,7 @@ const ChannelWithContext = < CommandsButton, compressImageQuality, CooldownTimer, + CreatePollContent, doDocUploadRequest, doImageUploadRequest, editing, @@ -2308,6 +2327,7 @@ const ChannelWithContext = < MoreOptionsButton, numberOfLines, onChangeText, + openPollCreationDialog, quotedMessage, SendButton, sendImageAsync, @@ -2372,6 +2392,8 @@ const ChannelWithContext = < handleReaction, handleRetry, handleThreadReply, + hasCreatePoll: + hasCreatePoll === undefined ? pollCreationEnabled : hasCreatePoll && pollCreationEnabled, ImageLoadingFailedIndicator, ImageLoadingIndicator, initialScrollToFirstUnreadMessage: !messageId && initialScrollToFirstUnreadMessage, // when messageId is set, we scroll to the messageId instead of first unread @@ -2405,6 +2427,7 @@ const ChannelWithContext = < onPressInMessage, onPressMessage, OverlayReactionList, + PollContent, ReactionList, removeMessage, Reply, diff --git a/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts b/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts index ff97c9e7d4..5ef1125d9d 100644 --- a/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts +++ b/package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts @@ -27,6 +27,7 @@ export const useCreateInputMessageInputContext = < CommandsButton, compressImageQuality, CooldownTimer, + CreatePollContent, doDocUploadRequest, doImageUploadRequest, editing, @@ -52,6 +53,7 @@ export const useCreateInputMessageInputContext = < MoreOptionsButton, numberOfLines, onChangeText, + openPollCreationDialog, quotedMessage, SendButton, sendImageAsync, @@ -59,6 +61,7 @@ export const useCreateInputMessageInputContext = < SendMessageDisallowedIndicator, setInputRef, setQuotedMessageState, + showPollCreationDialog, ShowThreadMessageInChannelButton, StartAudioRecordingButton, UploadProgressIndicator, @@ -97,6 +100,7 @@ export const useCreateInputMessageInputContext = < CommandsButton, compressImageQuality, CooldownTimer, + CreatePollContent, doDocUploadRequest, doImageUploadRequest, editing, @@ -122,6 +126,7 @@ export const useCreateInputMessageInputContext = < MoreOptionsButton, numberOfLines, onChangeText, + openPollCreationDialog, quotedMessage, SendButton, sendImageAsync, @@ -129,12 +134,22 @@ export const useCreateInputMessageInputContext = < SendMessageDisallowedIndicator, setInputRef, setQuotedMessageState, + showPollCreationDialog, ShowThreadMessageInChannelButton, StartAudioRecordingButton, UploadProgressIndicator, }), // eslint-disable-next-line react-hooks/exhaustive-deps - [compressImageQuality, channelId, editingDep, initialValue, maxMessageLength, quotedMessageId], + [ + compressImageQuality, + channelId, + editingDep, + initialValue, + maxMessageLength, + quotedMessageId, + CreatePollContent, + showPollCreationDialog, + ], ); return inputMessageInputContext; diff --git a/package/src/components/Channel/hooks/useCreateMessagesContext.ts b/package/src/components/Channel/hooks/useCreateMessagesContext.ts index a2d3bababd..3948abaad8 100644 --- a/package/src/components/Channel/hooks/useCreateMessagesContext.ts +++ b/package/src/components/Channel/hooks/useCreateMessagesContext.ts @@ -43,6 +43,7 @@ export const useCreateMessagesContext = < handleReaction, handleRetry, handleThreadReply, + hasCreatePoll, ImageLoadingFailedIndicator, ImageLoadingIndicator, initialScrollToFirstUnreadMessage, @@ -76,6 +77,7 @@ export const useCreateMessagesContext = < onPressInMessage, onPressMessage, OverlayReactionList, + PollContent, ReactionList, removeMessage, Reply, @@ -142,6 +144,7 @@ export const useCreateMessagesContext = < handleReaction, handleRetry, handleThreadReply, + hasCreatePoll, ImageLoadingFailedIndicator, ImageLoadingIndicator, initialScrollToFirstUnreadMessage, @@ -175,6 +178,7 @@ export const useCreateMessagesContext = < onPressInMessage, onPressMessage, OverlayReactionList, + PollContent, ReactionList, removeMessage, Reply, @@ -205,6 +209,7 @@ export const useCreateMessagesContext = < supportedReactionsLength, myMessageTheme, targetedMessage, + hasCreatePoll, ], ); diff --git a/package/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts b/package/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts index 0ab3f0f8e5..aec0fafa88 100644 --- a/package/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts +++ b/package/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts @@ -1,11 +1,20 @@ import { useEffect, useState } from 'react'; import { TFunction } from 'i18next'; -import type { Channel, ChannelState, MessageResponse, StreamChat, UserResponse } from 'stream-chat'; +import type { + Channel, + ChannelState, + MessageResponse, + PollState, + PollVote, + StreamChat, + UserResponse, +} from 'stream-chat'; import { useChatContext } from '../../../contexts/chatContext/ChatContext'; import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext'; +import { useStateStore } from '../../../hooks'; import { useTranslatedMessage } from '../../../hooks/useTranslatedMessage'; import type { DefaultStreamChatGenerics } from '../../../types/types'; import { stringifyMessage } from '../../../utils/utils'; @@ -28,6 +37,18 @@ export type LatestMessagePreview< created_at?: string | Date; }; +export type LatestMessagePreviewSelectorReturnType = { + created_by?: UserResponse | null; + latest_votes_by_option?: Record; +}; + +const selector = ( + nextValue: PollState, +): LatestMessagePreviewSelectorReturnType => ({ + created_by: nextValue.created_by, + latest_votes_by_option: nextValue.latest_votes_by_option, +}); + const getMessageSenderName = < StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, >( @@ -77,6 +98,7 @@ const getLatestMessageDisplayText = < client: StreamChat, message: LatestMessage | undefined, t: (key: string) => string, + pollState: LatestMessagePreviewSelectorReturnType | undefined, ) => { if (!message) return [{ bold: false, text: t('Nothing yet...') }]; const isMessageTypeDeleted = message.type === 'deleted'; @@ -123,6 +145,28 @@ const getLatestMessageDisplayText = < { bold: false, text: t('🏙 Attachment...') }, ]; } + if (message.poll && pollState) { + const { created_by, latest_votes_by_option } = pollState; + let latestVotes; + if (latest_votes_by_option) { + latestVotes = Object.values(latest_votes_by_option) + .map((votes) => votes?.[0]) + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); + } + let previewAction = 'created'; + let previewUser = created_by; + if (latestVotes && latestVotes.length && latestVotes[0]?.user) { + previewAction = 'voted'; + previewUser = latestVotes[0]?.user; + } + const previewMessage = `${ + client.userID === previewUser?.id ? 'You' : previewUser?.name + } ${previewAction}: ${message.poll.name}`; + return [ + { bold: false, text: '📊 ' }, + { bold: false, text: previewMessage }, + ]; + } return [ { bold: boldOwner, text: messageSenderText }, { bold: false, text: t('Empty message...') }, @@ -171,13 +215,14 @@ const getLatestMessagePreview = < >(params: { channel: Channel; client: StreamChat; + pollState: LatestMessagePreviewSelectorReturnType | undefined; readEvents: boolean; t: TFunction; lastMessage?: | ReturnType['formatMessage']> | MessageResponse; }) => { - const { channel, client, lastMessage, readEvents, t } = params; + const { channel, client, lastMessage, pollState, readEvents, t } = params; const messages = channel.state.messages; @@ -202,7 +247,7 @@ const getLatestMessagePreview = < return { created_at: message?.created_at, messageObject: message, - previews: getLatestMessageDisplayText(channel, client, message, t), + previews: getLatestMessageDisplayText(channel, client, message, t, pollState), status: getLatestMessageReadStatus(channel, client, message, readEvents), }; }; @@ -261,6 +306,12 @@ export const useLatestMessagePreview = < // eslint-disable-next-line react-hooks/exhaustive-deps }, [channelConfigExists]); + const pollId = lastMessage?.poll_id ?? ''; + const poll = client.polls.fromState(pollId); + const pollState: LatestMessagePreviewSelectorReturnType = + useStateStore(poll?.state, selector) ?? {}; + const { created_by, latest_votes_by_option } = pollState; + useEffect( () => setLatestMessagePreview( @@ -268,12 +319,20 @@ export const useLatestMessagePreview = < channel, client, lastMessage: translatedLastMessage, + pollState, readEvents, t, }), ), // eslint-disable-next-line react-hooks/exhaustive-deps - [channelLastMessageString, forceUpdate, readEvents, readStatus], + [ + channelLastMessageString, + forceUpdate, + readEvents, + readStatus, + latest_votes_by_option, + created_by, + ], ); return latestMessagePreview; diff --git a/package/src/components/Chat/Chat.tsx b/package/src/components/Chat/Chat.tsx index 54ad2ac36c..2e2999408c 100644 --- a/package/src/components/Chat/Chat.tsx +++ b/package/src/components/Chat/Chat.tsx @@ -219,9 +219,11 @@ const ChatWithContext = < if (!client) return; client.threads.registerSubscriptions(); + client.polls.registerSubscriptions(); return () => { client.threads.unregisterSubscriptions(); + client.polls.unregisterSubscriptions(); }; }, [client]); diff --git a/package/src/components/Chat/hooks/handleEventToSyncDB.ts b/package/src/components/Chat/hooks/handleEventToSyncDB.ts index d93bf5715f..6734645e7c 100644 --- a/package/src/components/Chat/hooks/handleEventToSyncDB.ts +++ b/package/src/components/Chat/hooks/handleEventToSyncDB.ts @@ -4,6 +4,7 @@ import { deleteChannel } from '../../../store/apis/deleteChannel'; import { deleteMember } from '../../../store/apis/deleteMember'; import { deleteMessagesForChannel } from '../../../store/apis/deleteMessagesForChannel'; import { updateMessage } from '../../../store/apis/updateMessage'; +import { updatePollMessage } from '../../../store/apis/updatePollMessage'; import { upsertChannelData } from '../../../store/apis/upsertChannelData'; import { upsertChannelDataFromChannel } from '../../../store/apis/upsertChannelDataFromChannel'; import { upsertChannels } from '../../../store/apis/upsertChannels'; @@ -223,5 +224,23 @@ export const handleEventToSyncDB = < } } + if ( + [ + 'poll.closed', + 'poll.updated', + 'poll.vote_casted', + 'poll.vote_changed', + 'poll.vote_removed', + ].includes(type) + ) { + const poll = event.poll; + if (poll) { + return updatePollMessage({ + flush, + poll, + }); + } + } + return []; }; diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index 4ef1a028fd..ce5b4f700d 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { GestureResponderEvent, Keyboard, StyleProp, View, ViewStyle } from 'react-native'; import type { Attachment, UserResponse } from 'stream-chat'; @@ -54,6 +54,7 @@ import { MessageType, } from '../MessageList/hooks/useMessageList'; import type { MessageActionListItemProps } from '../MessageOverlay/MessageActionListItem'; +import { Poll as PollComponent } from '../Poll'; export type TouchableEmitter = | 'fileAttachment' @@ -175,6 +176,7 @@ export type MessagePropsWithContext< | 'setQuotedMessageState' | 'supportedReactions' | 'updateMessage' + | 'PollContent' > & Pick, 'setData'> & Pick & @@ -284,6 +286,7 @@ const MessageWithContext = < onThreadSelect, openThread, OverlayReactionList, + PollContent, preventPress, removeMessage, retrySendMessage, @@ -451,6 +454,8 @@ const MessageWithContext = < return !!attachments.files.length; case 'gallery': return !!attachments.images.length || !!attachments.videos.length; + case 'poll': + return !!message.poll_id; case 'text': default: return !!message.text; @@ -561,6 +566,14 @@ const MessageWithContext = < const { userLanguage } = useTranslationContext(); + // TODO: Can be removed in V6 and from here completely once it becomes baseline. + const PollWrapper = useCallback(() => { + const poll = message?.poll_id ? client.polls.fromState(message.poll_id) : undefined; + return message?.poll_id && poll ? ( + message={message} poll={poll} PollContent={PollContent} /> + ) : null; + }, [PollContent, client, message]); + const showMessageOverlay = async (isMessageActionsVisible = true, error = errorOrFailed) => { await dismissKeyboard(); @@ -611,6 +624,7 @@ const MessageWithContext = < otherAttachments: attachments.other, OverlayReactionList, ownCapabilities, + Poll: PollWrapper, supportedReactions, threadList, userLanguage, diff --git a/package/src/components/Message/MessageSimple/MessageContent.tsx b/package/src/components/Message/MessageSimple/MessageContent.tsx index a2e243f79e..4a6a20fe9d 100644 --- a/package/src/components/Message/MessageSimple/MessageContent.tsx +++ b/package/src/components/Message/MessageSimple/MessageContent.tsx @@ -9,6 +9,7 @@ import { import { MessageTextContainer } from './MessageTextContainer'; +import { useChatContext } from '../../../contexts'; import { MessageContextValue, useMessageContext, @@ -26,6 +27,7 @@ import { import { useViewport } from '../../../hooks/useViewport'; import type { DefaultStreamChatGenerics } from '../../../types/types'; import { MessageStatusTypes } from '../../../utils/utils'; +import { Poll } from '../../Poll/Poll'; const styles = StyleSheet.create({ containerInner: { @@ -142,6 +144,8 @@ const MessageContentWithContext = < showMessageStatus, threadList, } = props; + const { client } = useChatContext(); + const { PollContent: PollContentOverride } = useMessagesContext(); const { theme: { @@ -377,6 +381,18 @@ const MessageContentWithContext = < ); case 'gallery': return ; + case 'poll': { + const pollId = message.poll_id; + const poll = pollId && client.polls.fromState(pollId); + return pollId && poll ? ( + + ) : null; + } case 'text': default: return otherAttachments.length && otherAttachments[0].actions ? null : ( diff --git a/package/src/components/MessageInput/MessageInput.tsx b/package/src/components/MessageInput/MessageInput.tsx index 20a9cf52fb..f4140b0cab 100644 --- a/package/src/components/MessageInput/MessageInput.tsx +++ b/package/src/components/MessageInput/MessageInput.tsx @@ -1,9 +1,17 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { NativeSyntheticEvent, StyleSheet, TextInputFocusEventData, View } from 'react-native'; +import { + Modal, + NativeSyntheticEvent, + SafeAreaView, + StyleSheet, + TextInputFocusEventData, + View, +} from 'react-native'; import { Gesture, GestureDetector, + GestureHandlerRootView, PanGestureHandlerEventPayload, } from 'react-native-gesture-handler'; import Animated, { @@ -51,6 +59,7 @@ import { import { isImageMediaLibraryAvailable, triggerHaptic } from '../../native'; import type { Asset, DefaultStreamChatGenerics } from '../../types/types'; import { AutoCompleteInput } from '../AutoCompleteInput/AutoCompleteInput'; +import { CreatePoll } from '../Poll/CreatePollContent'; const styles = StyleSheet.create({ attachmentSeparator: { @@ -145,6 +154,11 @@ type MessageInputPropsWithContext< | 'text' | 'uploadNewFile' | 'uploadNewImage' + | 'openPollCreationDialog' + | 'closePollCreationDialog' + | 'showPollCreationDialog' + | 'sendMessage' + | 'CreatePollContent' > & Pick, 'Reply'> & Pick< @@ -179,8 +193,10 @@ const MessageInputWithContext = < AudioRecordingPreview, AutoCompleteSuggestionList, closeAttachmentPicker, + closePollCreationDialog, cooldownEndsAt, CooldownTimer, + CreatePollContent, editing, FileUploadPreview, fileUploads, @@ -206,8 +222,10 @@ const MessageInputWithContext = < resetInput, SendButton, sending, + sendMessage, sendMessageAsync, setShowMoreOptions, + showPollCreationDialog, ShowThreadMessageInChannelButton, StartAudioRecordingButton, suggestions, @@ -876,6 +894,23 @@ const MessageInputWithContext = < )} + {showPollCreationDialog ? ( + + + + + + + + ) : null} ); }; @@ -891,6 +926,7 @@ const areEqual = { @@ -32,6 +33,7 @@ describe('MessageInput', () => { AttachmentPickerSelectionBar, CameraSelectorIcon, closePicker: jest.fn(), + CreatePollIcon, FileSelectorIcon, ImageSelectorIcon, openPicker: jest.fn(), diff --git a/package/src/components/MessageInput/components/NativeAttachmentPicker.tsx b/package/src/components/MessageInput/components/NativeAttachmentPicker.tsx index 762a7cb36e..4e989d29b8 100644 --- a/package/src/components/MessageInput/components/NativeAttachmentPicker.tsx +++ b/package/src/components/MessageInput/components/NativeAttachmentPicker.tsx @@ -1,12 +1,18 @@ import React, { useEffect, useRef } from 'react'; import { Animated, Easing, LayoutRectangle, Pressable, StyleSheet } from 'react-native'; +import { + useChannelContext, + useMessagesContext, + useOwnCapabilitiesContext, +} from '../../../contexts'; import { useMessageInputContext } from '../../../contexts/messageInputContext/MessageInputContext'; import { useTheme } from '../../../contexts/themeContext/ThemeContext'; import { CameraSelectorIcon } from '../../AttachmentPicker/components/CameraSelectorIcon'; import { FileSelectorIcon } from '../../AttachmentPicker/components/FileSelectorIcon'; import { ImageSelectorIcon } from '../../AttachmentPicker/components/ImageSelectorIcon'; +import { CreatePollIcon } from '../../Poll/components/CreatePollIcon'; type NativeAttachmentPickerProps = { onRequestedClose: () => void; @@ -38,10 +44,15 @@ export const NativeAttachmentPicker = ({ const { hasFilePicker, hasImagePicker, + openPollCreationDialog, pickAndUploadImageFromNativePicker, pickFile, + sendMessage, takeAndUploadImage, } = useMessageInputContext(); + const { threadList } = useChannelContext(); + const { hasCreatePoll } = useMessagesContext(); + const ownCapabilities = useOwnCapabilitiesContext(); const popupHeight = // the top padding @@ -91,7 +102,11 @@ export const NativeAttachmentPicker = ({ width: size, }; - const onClose = ({ onPressHandler }: { onPressHandler?: () => Promise }) => { + const onClose = ({ + onPressHandler, + }: { + onPressHandler?: (() => Promise) | (() => void); + }) => { if (onPressHandler) { onPressHandler(); } @@ -103,7 +118,19 @@ export const NativeAttachmentPicker = ({ }).start(onRequestedClose); }; - const buttons = []; + // do not allow poll creation in threads + const buttons = + threadList && hasCreatePoll && ownCapabilities.sendPoll + ? [] + : [ + { + icon: , + id: 'Poll', + onPressHandler: () => { + openPollCreationDialog?.({ sendMessage }); + }, + }, + ]; if (hasImagePicker) { buttons.push({ diff --git a/package/src/components/MessageOverlay/MessageOverlay.tsx b/package/src/components/MessageOverlay/MessageOverlay.tsx index 00aa850776..ae206e1f75 100644 --- a/package/src/components/MessageOverlay/MessageOverlay.tsx +++ b/package/src/components/MessageOverlay/MessageOverlay.tsx @@ -25,6 +25,7 @@ import { MessageActionList as DefaultMessageActionList } from './MessageActionLi import { OverlayReactionList as OverlayReactionListDefault } from './OverlayReactionList'; import { OverlayReactionsAvatar as OverlayReactionsAvatarDefault } from './OverlayReactionsAvatar'; +import { OwnCapabilitiesProvider } from '../../contexts'; import { ChatProvider } from '../../contexts/chatContext/ChatContext'; import { MessageProvider } from '../../contexts/messageContext/MessageContext'; import { @@ -135,6 +136,7 @@ const MessageOverlayWithContext = < OverlayReactions = DefaultOverlayReactions, OverlayReactionsAvatar = OverlayReactionsAvatarDefault, ownCapabilities, + Poll, setOverlay, threadList, videos, @@ -441,6 +443,14 @@ const MessageOverlayWithContext = < /> ) ); + case 'poll': { + const pollId = message.poll_id; + return Poll && pollId && ownCapabilities ? ( + + + + ) : null; + } case 'text': default: return otherAttachments?.length && otherAttachments[0].actions ? null : ( diff --git a/package/src/components/Poll/CreatePollContent.tsx b/package/src/components/Poll/CreatePollContent.tsx new file mode 100644 index 0000000000..4c7a41ccf7 --- /dev/null +++ b/package/src/components/Poll/CreatePollContent.tsx @@ -0,0 +1,299 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { StyleSheet, Switch, Text, TextInput, TouchableOpacity, View } from 'react-native'; + +import { ScrollView } from 'react-native-gesture-handler'; +import { useSharedValue } from 'react-native-reanimated'; + +import { CreatePollData, PollOptionData, VotingVisibility } from 'stream-chat'; + +import { CreatePollOptions, CurrentOptionPositionsCache, PollModalHeader } from './components'; + +import { + CreatePollContentContextValue, + CreatePollContentProvider, + InputMessageInputContextValue, + useChatContext, + useCreatePollContentContext, + useTheme, + useTranslationContext, +} from '../../contexts'; +import { SendPoll } from '../../icons'; + +export const isMaxNumberOfVotesValid = (maxNumberOfVotes: string) => { + const parsedMaxNumberOfVotes = Number(maxNumberOfVotes); + + return ( + !isNaN(parsedMaxNumberOfVotes) && parsedMaxNumberOfVotes > 1 && parsedMaxNumberOfVotes <= 10 + ); +}; + +export const CreatePollContent = () => { + const { t } = useTranslationContext(); + const [pollTitle, setPollTitle] = useState(''); + const [pollOptions, setPollOptions] = useState([{ text: '' }]); + const [multipleAnswersAllowed, setMultipleAnswersAllowed] = useState(false); + const [maxVotesPerPerson, setMaxVotesPerPerson] = useState(''); + const [maxVotesPerPersonEnabled, setMaxVotesPerPersonEnabled] = useState(false); + const [isAnonymous, setIsAnonymous] = useState(false); + const [optionSuggestionsAllowed, setOptionSuggestionsAllowed] = useState(false); + const [commentsAllowed, setCommentsAllowed] = useState(false); + const [duplicates, setDuplicates] = useState([]); + + const { closePollCreationDialog, createAndSendPoll } = useCreatePollContentContext(); + + // positions and index lookup map + // TODO: Please rethink the structure of this, bidirectional data flow is not great + const currentOptionPositions = useSharedValue({ + inverseIndexCache: { 0: 0 }, + positionCache: { 0: { updatedIndex: 0, updatedTop: 0 } }, + }); + + const { + theme: { + colors: { accent_error, bg_user, black, white }, + poll: { + createContent: { + addComment, + anonymousPoll, + headerContainer, + maxVotes, + multipleAnswers, + name, + scrollView, + sendButton, + suggestOption, + }, + }, + }, + } = useTheme(); + + useEffect(() => { + const seenTexts = new Set(); + const duplicateTexts = new Set(); + for (const option of pollOptions) { + const { text } = option; + if (seenTexts.has(text)) { + duplicateTexts.add(text); + } + if (text.length > 0) { + seenTexts.add(text); + } + } + + setDuplicates(Array.from(duplicateTexts)); + }, [pollOptions]); + + const isPollValid = + pollTitle && + pollTitle?.length > 0 && + duplicates.length === 0 && + ((maxVotesPerPersonEnabled && isMaxNumberOfVotesValid(maxVotesPerPerson)) || + !maxVotesPerPersonEnabled); + + return ( + <> + + closePollCreationDialog?.()} title={t('Create Poll')} /> + { + const currentPollOptions = Object.assign({}, pollOptions); + const reorderedPollOptions = []; + + for (let i = 0; i < pollOptions.length; i++) { + const currentOption = + currentPollOptions[currentOptionPositions.value.inverseIndexCache[i]]; + if (currentOption.text.length > 0) { + reorderedPollOptions.push(currentOption); + } + } + + createAndSendPoll({ + allow_answers: commentsAllowed, + allow_user_suggested_options: optionSuggestionsAllowed, + enforce_unique_vote: !multipleAnswersAllowed, + name: pollTitle, + options: reorderedPollOptions, + voting_visibility: isAnonymous ? VotingVisibility.anonymous : VotingVisibility.public, + ...(isMaxNumberOfVotesValid(maxVotesPerPerson) && maxVotesPerPersonEnabled + ? { max_votes_allowed: Number(maxVotesPerPerson) } + : {}), + }); + }} + style={[styles.sendButton, sendButton]} + > + + + + + {t('Questions')} + + + + + + {t('Multiple answers')} + + setMultipleAnswersAllowed(!multipleAnswersAllowed)} + value={multipleAnswersAllowed} + /> + + {multipleAnswersAllowed ? ( + + {maxVotesPerPersonEnabled && !isMaxNumberOfVotesValid(maxVotesPerPerson) ? ( + + {t('Type a number from 2 to 10')} + + ) : null} + + + setMaxVotesPerPersonEnabled(!maxVotesPerPersonEnabled)} + value={maxVotesPerPersonEnabled} + /> + + + ) : null} + + + + {t('Anonymous poll')} + + setIsAnonymous(!isAnonymous)} value={isAnonymous} /> + + + + {t('Suggest an option')} + + setOptionSuggestionsAllowed(!optionSuggestionsAllowed)} + value={optionSuggestionsAllowed} + /> + + + + {t('Add a comment')} + + setCommentsAllowed(!commentsAllowed)} + value={commentsAllowed} + /> + + + + ); +}; + +export const CreatePoll = ({ + closePollCreationDialog, + CreatePollContent: CreatePollContentOverride, + createPollOptionHeight, + sendMessage, +}: Pick< + CreatePollContentContextValue, + 'createPollOptionHeight' | 'closePollCreationDialog' | 'sendMessage' +> & + Pick) => { + const { client } = useChatContext(); + + const createAndSendPoll = useCallback( + async (pollData: CreatePollData) => { + const poll = await client.polls.createPoll(pollData); + await sendMessage({ customMessageData: { poll_id: poll.id } }); + closePollCreationDialog?.(); + }, + [client, sendMessage, closePollCreationDialog], + ); + + return ( + + {CreatePollContentOverride ? : } + + ); +}; + +const styles = StyleSheet.create({ + headerContainer: { flexDirection: 'row', justifyContent: 'space-between' }, + maxVotesInput: { flex: 1, fontSize: 16 }, + maxVotesValidationText: { + fontSize: 12, + left: 16, + position: 'absolute', + top: 0, + }, + maxVotesWrapper: { + alignItems: 'flex-start', + flexDirection: 'column', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 18, + }, + multipleAnswersRow: { + alignItems: 'center', + flexDirection: 'row', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 18, + }, + multipleAnswersWrapper: { borderRadius: 12, marginTop: 16 }, + scrollView: { flex: 1, padding: 16 }, + sendButton: { paddingHorizontal: 16, paddingVertical: 18 }, + text: { fontSize: 16 }, + textInputWrapper: { + alignItems: 'center', + borderRadius: 12, + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 16, + paddingHorizontal: 16, + paddingVertical: 18, + }, +}); diff --git a/package/src/components/Poll/Poll.tsx b/package/src/components/Poll/Poll.tsx new file mode 100644 index 0000000000..8d3966d904 --- /dev/null +++ b/package/src/components/Poll/Poll.tsx @@ -0,0 +1,126 @@ +import React, { useMemo } from 'react'; +import { StyleSheet, Text, View } from 'react-native'; + +import { PollOption as PollOptionClass } from 'stream-chat'; + +import { + AddCommentButton, + EndVoteButton, + PollOption, + ShowAllCommentsButton, + ShowAllOptionsButton, + SuggestOptionButton, + ViewResultsButton, +} from './components'; + +import { usePollState } from './hooks/usePollState'; + +import { + MessagesContextValue, + PollContextProvider, + PollContextValue, + useTheme, + useTranslationContext, +} from '../../contexts'; +import type { DefaultStreamChatGenerics } from '../../types/types'; + +export type PollProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = Pick, 'poll' | 'message'> & + Pick, 'PollContent'>; + +export type PollContentProps = { + PollButtons?: React.ComponentType; + PollHeader?: React.ComponentType; +}; + +export const PollButtons = () => ( + <> + + + + + + + +); + +export const PollHeader = () => { + const { t } = useTranslationContext(); + const { enforce_unique_vote, is_closed, max_votes_allowed, name } = usePollState(); + const subtitle = useMemo(() => { + if (is_closed) return t('Vote ended'); + if (enforce_unique_vote) return t('Select one'); + if (max_votes_allowed) return t('Select up to {{count}}', { count: max_votes_allowed }); + return t('Select one or more'); + }, [is_closed, t, enforce_unique_vote, max_votes_allowed]); + + const { + theme: { + colors: { text_high_emphasis, text_low_emphasis }, + poll: { + message: { header }, + }, + }, + } = useTheme(); + + return ( + <> + {name} + + {subtitle} + + + ); +}; + +export const PollContent = ({ + PollButtons: PollButtonsOverride, + PollHeader: PollHeaderOverride, +}: PollContentProps) => { + const { options } = usePollState(); + + const { + theme: { + poll: { + message: { container, optionsWrapper }, + }, + }, + } = useTheme(); + + return ( + + {PollHeaderOverride ? : } + + {options?.slice(0, 10)?.map((option: PollOptionClass) => ( + + ))} + + {PollButtonsOverride ? : } + + ); +}; + +export const Poll = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>({ + message, + poll, + PollContent: PollContentOverride, +}: PollProps) => ( + + {PollContentOverride ? : } + +); + +const styles = StyleSheet.create({ + container: { padding: 15, width: 270 }, + headerSubtitle: { fontSize: 12, marginTop: 4 }, + headerTitle: { fontSize: 16, fontWeight: '500' }, + optionsWrapper: { marginTop: 12 }, +}); diff --git a/package/src/components/Poll/components/Button.tsx b/package/src/components/Poll/components/Button.tsx new file mode 100644 index 0000000000..527416dc19 --- /dev/null +++ b/package/src/components/Poll/components/Button.tsx @@ -0,0 +1,465 @@ +import React, { useCallback, useState } from 'react'; +import { Modal, SafeAreaView, StyleSheet, Text, TouchableOpacity } from 'react-native'; + +import { Poll, PollOption } from 'stream-chat'; + +import { PollAnswersList } from './PollAnswersList'; +import { PollInputDialog } from './PollInputDialog'; +import { PollModalHeader } from './PollModalHeader'; +import { PollAllOptions } from './PollOption'; + +import { PollOptionFullResults, PollResults } from './PollResults'; + +import { + useChatContext, + useOwnCapabilitiesContext, + usePollContext, + useTheme, + useTranslationContext, +} from '../../../contexts'; +import { Check } from '../../../icons'; +import type { DefaultStreamChatGenerics } from '../../../types/types'; +import { MessageType } from '../../MessageList/hooks/useMessageList'; +import { usePollState } from '../hooks/usePollState'; + +export type PollButtonProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = { + onPress?: ({ + message, + poll, + }: { + message: MessageType; + poll: Poll; + }) => void; +}; + +export type ShowAllVotesButtonProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = { + option: PollOption; + onPress?: ({ + message, + option, + poll, + }: { + message: MessageType; + option: PollOption; + poll: Poll; + }) => void; +}; + +export type PollVoteButtonProps = { + option: PollOption; +} & Pick; + +export const GenericPollButton = ({ onPress, title }: { onPress?: () => void; title?: string }) => { + const { + theme: { + colors: { accent_dark_blue }, + poll: { + button: { container, text }, + }, + }, + } = useTheme(); + + return ( + + {title} + + ); +}; + +export const ViewResultsButton = (props: PollButtonProps) => { + const { t } = useTranslationContext(); + const { message, poll } = usePollContext(); + const [showResults, setShowResults] = useState(false); + const { onPress } = props; + + const onPressHandler = useCallback(() => { + if (onPress) { + onPress({ message, poll }); + return; + } + + setShowResults(true); + }, [message, onPress, poll]); + + const { + theme: { + colors: { white }, + }, + } = useTheme(); + + return ( + <> + ('View Results')} /> + {showResults ? ( + setShowResults(false)} + visible={showResults} + > + + setShowResults(false)} + title={t('Poll Results')} + /> + + + + ) : null} + + ); +}; + +export const EndVoteButton = () => { + const { t } = useTranslationContext(); + const { created_by, endVote, is_closed } = usePollState(); + const { client } = useChatContext(); + + return !is_closed && created_by?.id === client.userID ? ( + ('End Vote')} /> + ) : null; +}; + +export const AddCommentButton = (props: PollButtonProps) => { + const { t } = useTranslationContext(); + const { message, poll } = usePollContext(); + const { addComment, allow_answers, is_closed, ownAnswer } = usePollState(); + const [showAddCommentDialog, setShowAddCommentDialog] = useState(false); + const { onPress } = props; + + const onPressHandler = useCallback(() => { + if (onPress) { + onPress({ message, poll }); + return; + } + + setShowAddCommentDialog(true); + }, [message, onPress, poll]); + + return ( + <> + {!is_closed && allow_answers ? ( + ('Add a comment')} /> + ) : null} + {showAddCommentDialog ? ( + setShowAddCommentDialog(false)} + initialValue={ownAnswer?.answer_text ?? ''} + onSubmit={addComment} + title={t('Add a comment')} + visible={showAddCommentDialog} + /> + ) : null} + + ); +}; + +export const ShowAllCommentsButton = (props: PollButtonProps) => { + const { t } = useTranslationContext(); + const { message, poll } = usePollContext(); + const { answers_count } = usePollState(); + const [showAnswers, setShowAnswers] = useState(false); + const { onPress } = props; + + const onPressHandler = useCallback(() => { + if (onPress) { + onPress({ message, poll }); + return; + } + + setShowAnswers(true); + }, [message, onPress, poll]); + + const { + theme: { + colors: { white }, + }, + } = useTheme(); + + return ( + <> + {answers_count && answers_count > 0 ? ( + ('View {{count}} comments', { count: answers_count })} + /> + ) : null} + {showAnswers ? ( + setShowAnswers(false)} + visible={showAnswers} + > + + setShowAnswers(false)} + title={t('Poll Comments')} + /> + + + + ) : null} + + ); +}; + +export const AnswerListAddCommentButton = (props: PollButtonProps) => { + const { t } = useTranslationContext(); + const { message, poll } = usePollContext(); + const { addComment, ownAnswer } = usePollState(); + const [showAddCommentDialog, setShowAddCommentDialog] = useState(false); + const { onPress } = props; + + const onPressHandler = useCallback(() => { + if (onPress) { + onPress({ message, poll }); + return; + } + + setShowAddCommentDialog(true); + }, [message, onPress, poll]); + + const { + theme: { + colors: { accent_dark_blue, bg_user }, + poll: { + answersList: { buttonContainer }, + button: { text }, + }, + }, + } = useTheme(); + + return ( + <> + + + {ownAnswer ? t('Update your comment') : t('Add a comment')} + + + {showAddCommentDialog ? ( + setShowAddCommentDialog(false)} + initialValue={ownAnswer?.answer_text ?? ''} + onSubmit={addComment} + title={t('Add a comment')} + visible={showAddCommentDialog} + /> + ) : null} + + ); +}; + +export const SuggestOptionButton = (props: PollButtonProps) => { + const { t } = useTranslationContext(); + const { message, poll } = usePollContext(); + const { addOption, allow_user_suggested_options, is_closed } = usePollState(); + const [showAddOptionDialog, setShowAddOptionDialog] = useState(false); + const { onPress } = props; + + const onPressHandler = useCallback(() => { + if (onPress) { + onPress({ message, poll }); + return; + } + + setShowAddOptionDialog(true); + }, [message, onPress, poll]); + + return ( + <> + {!is_closed && allow_user_suggested_options ? ( + ('Suggest an option')} /> + ) : null} + {showAddOptionDialog ? ( + setShowAddOptionDialog(false)} + onSubmit={addOption} + title={t('Suggest an option')} + visible={showAddOptionDialog} + /> + ) : null} + + ); +}; + +export const ShowAllOptionsButton = (props: PollButtonProps) => { + const { t } = useTranslationContext(); + const [showAllOptions, setShowAllOptions] = useState(false); + const { message, poll } = usePollContext(); + const { options } = usePollState(); + const { onPress } = props; + + const onPressHandler = useCallback(() => { + if (onPress) { + onPress({ message, poll }); + return; + } + + setShowAllOptions(true); + }, [message, onPress, poll]); + + const { + theme: { + colors: { white }, + }, + } = useTheme(); + + return ( + <> + {options && options.length > 10 ? ( + ('See all {{count}} options', { count: options.length })} + /> + ) : null} + {showAllOptions ? ( + setShowAllOptions(false)} + visible={showAllOptions} + > + + setShowAllOptions(false)} + title={t('Poll Options')} + /> + + + + ) : null} + + ); +}; + +export const VoteButton = ({ onPress, option }: PollVoteButtonProps) => { + const { message, poll } = usePollContext(); + const { is_closed, ownVotesByOptionId } = usePollState(); + const ownCapabilities = useOwnCapabilitiesContext(); + + const { + theme: { + colors: { accent_dark_blue, disabled }, + poll: { + message: { + option: { voteButtonActive, voteButtonContainer, voteButtonInactive }, + }, + }, + }, + } = useTheme(); + + const toggleVote = useCallback(async () => { + if (ownVotesByOptionId[option.id]) { + await poll.removeVote(ownVotesByOptionId[option.id]?.id, message.id); + } else { + await poll.castVote(option.id, message.id); + } + }, [message.id, option.id, ownVotesByOptionId, poll]); + + const onPressHandler = useCallback(() => { + if (onPress) { + onPress({ message, poll }); + return; + } + + toggleVote(); + }, [message, onPress, poll, toggleVote]); + + return ownCapabilities.castPollVote && !is_closed ? ( + + {ownVotesByOptionId[option.id] ? : null} + + ) : null; +}; + +export const ShowAllVotesButton = (props: ShowAllVotesButtonProps) => { + const { t } = useTranslationContext(); + const { message, poll } = usePollContext(); + const { vote_counts_by_option } = usePollState(); + const ownCapabilities = useOwnCapabilitiesContext(); + const [showAllVotes, setShowAllVotes] = useState(false); + const { onPress, option } = props; + + const onPressHandler = useCallback(() => { + if (onPress) { + onPress({ message, option, poll }); + return; + } + + setShowAllVotes(true); + }, [message, onPress, option, poll]); + + const { + theme: { + colors: { white }, + }, + } = useTheme(); + + return ( + <> + {ownCapabilities.queryPollVotes && + vote_counts_by_option && + vote_counts_by_option?.[option.id] > 5 ? ( + ('Show All')} /> + ) : null} + {showAllVotes ? ( + setShowAllVotes(false)} + visible={showAllVotes} + > + + setShowAllVotes(false)} title={option.text} /> + + + + ) : null} + + ); +}; + +const styles = StyleSheet.create({ + answerListAddCommentContainer: { + alignItems: 'center', + borderRadius: 12, + paddingHorizontal: 16, + paddingVertical: 18, + }, + container: { + alignItems: 'center', + marginHorizontal: 16, + paddingVertical: 11, + }, + text: { fontSize: 16 }, + voteContainer: { + alignItems: 'center', + borderRadius: 18, + borderWidth: 1, + height: 18, + justifyContent: 'center', + width: 18, + }, +}); diff --git a/package/src/components/Poll/components/CreatePollIcon.tsx b/package/src/components/Poll/components/CreatePollIcon.tsx new file mode 100644 index 0000000000..ebceaa8f7b --- /dev/null +++ b/package/src/components/Poll/components/CreatePollIcon.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { useTheme } from '../../../contexts'; +import { PollThumbnail } from '../../../icons'; + +export const CreatePollIcon = () => { + const { + theme: { + colors: { grey }, + }, + } = useTheme(); + + return ; +}; diff --git a/package/src/components/Poll/components/CreatePollOptions.tsx b/package/src/components/Poll/components/CreatePollOptions.tsx new file mode 100644 index 0000000000..4a272f4591 --- /dev/null +++ b/package/src/components/Poll/components/CreatePollOptions.tsx @@ -0,0 +1,351 @@ +import React, { Dispatch, SetStateAction, useCallback, useMemo } from 'react'; +import { StyleSheet, Text, TextInput, TouchableOpacity, View } from 'react-native'; +import { Gesture, GestureDetector } from 'react-native-gesture-handler'; +import Animated, { + interpolate, + SharedValue, + useAnimatedReaction, + useAnimatedStyle, + useDerivedValue, + useSharedValue, + withDelay, + withSpring, +} from 'react-native-reanimated'; + +import { PollOptionData } from 'stream-chat'; + +import { useCreatePollContentContext, useTheme, useTranslationContext } from '../../../contexts'; +import { DragHandle } from '../../../icons'; + +export type CurrentOptionPositionsCache = { + inverseIndexCache: { + [key: number]: number; + }; + positionCache: { + [key: number]: { + updatedIndex: number; + updatedTop: number; + }; + }; +}; + +const OPTION_HEIGHT = 71; + +export const CreatePollOption = ({ + boundaries, + currentOptionPositions, + draggedItemId, + handleChangeText, + hasDuplicate, + index, + isDragging, + option, +}: { + boundaries: { maxBound: number; minBound: number }; + currentOptionPositions: SharedValue; + draggedItemId: SharedValue; + handleChangeText: (newText: string, index: number) => void; + hasDuplicate: boolean; + index: number; + isDragging: SharedValue<1 | 0>; + option: PollOptionData; +}) => { + const { t } = useTranslationContext(); + const { createPollOptionHeight = OPTION_HEIGHT } = useCreatePollContentContext(); + const top = useSharedValue(index * createPollOptionHeight); + const isDraggingDerived = useDerivedValue(() => isDragging.value); + + const draggedItemIdDerived = useDerivedValue(() => draggedItemId.value); + + const isCurrentDraggingItem = useDerivedValue( + () => isDraggingDerived.value && draggedItemIdDerived.value === index, + ); + + const animatedStyles = useAnimatedStyle(() => ({ + top: top.value, + transform: [ + { + scale: isCurrentDraggingItem.value + ? interpolate(isDraggingDerived.value, [0, 1], [1, 1.025]) + : interpolate(isDraggingDerived.value, [0, 1], [1, 0.98]), + }, + ], + })); + const currentOptionPositionsDerived = useDerivedValue( + () => currentOptionPositions.value, + ); + + // used for swapping with currentIndex + const newIndex = useSharedValue(null); + + // used for swapping with newIndex + const currentIndex = useSharedValue(null); + + useAnimatedReaction( + () => currentOptionPositionsDerived.value.positionCache[index].updatedIndex, + (currentValue, previousValue) => { + if (currentValue !== previousValue) { + top.value = withSpring( + currentOptionPositionsDerived.value.positionCache[index].updatedIndex * + createPollOptionHeight, + ); + } + }, + ); + + const gesture = Gesture.Pan() + .onStart(() => { + // start dragging + isDragging.value = withSpring(1); + + // keep track of dragged item + draggedItemId.value = index; + + // store dragged item id for future swap + currentIndex.value = currentOptionPositionsDerived.value.positionCache[index].updatedIndex; + }) + .onUpdate((e) => { + const { inverseIndexCache, positionCache } = currentOptionPositionsDerived.value; + if (draggedItemIdDerived.value === null || currentIndex.value === null) { + return; + } + const newTop = positionCache[draggedItemIdDerived.value].updatedTop + e.translationY; + // we add a small leeway to account for sharp animations which tend to bug out otherwise + if (newTop < boundaries.minBound - 10 || newTop > boundaries.maxBound + 10) { + // out of bounds, exit out of the animation early + return; + } + top.value = newTop; + + // calculate the new index where drag is headed to + newIndex.value = Math.floor((newTop + createPollOptionHeight / 2) / createPollOptionHeight); + + // swap the items present at newIndex and currentIndex + if (newIndex.value !== currentIndex.value) { + // find id of the item that currently resides at newIndex + const newIndexItemKey = inverseIndexCache[newIndex.value]; + + // find id of the item that currently resides at currentIndex + const currentDragIndexItemKey = inverseIndexCache[currentIndex.value]; + + if (newIndexItemKey !== undefined && currentDragIndexItemKey !== undefined) { + // if we indeed have a candidate for a new index, we update our cache so that + // it can be reflected through animations + currentOptionPositions.value = { + inverseIndexCache: { + ...inverseIndexCache, + [newIndex.value]: currentDragIndexItemKey, + [currentIndex.value]: newIndexItemKey, + }, + positionCache: { + ...positionCache, + [currentDragIndexItemKey]: { + ...positionCache[currentDragIndexItemKey], + updatedIndex: newIndex.value, + }, + [newIndexItemKey]: { + ...positionCache[newIndexItemKey], + updatedIndex: currentIndex.value, + updatedTop: currentIndex.value * createPollOptionHeight, + }, + }, + }; + + // update new index as current index + currentIndex.value = newIndex.value; + } + } + }) + .onEnd(() => { + const { inverseIndexCache, positionCache } = currentOptionPositionsDerived.value; + if (currentIndex.value === null || newIndex.value === null) { + return; + } + + top.value = withSpring(newIndex.value * createPollOptionHeight); + + // find original id of the item that currently resides at currentIndex + const currentDragIndexItemKey = inverseIndexCache[currentIndex.value]; + + if (currentDragIndexItemKey !== undefined) { + // update the values for item whose drag we just stopped + currentOptionPositions.value = { + ...currentOptionPositionsDerived.value, + positionCache: { + ...positionCache, + [currentDragIndexItemKey]: { + ...positionCache[currentDragIndexItemKey], + updatedTop: newIndex.value * createPollOptionHeight, + }, + }, + }; + } + // stop dragging + isDragging.value = withDelay(200, withSpring(0)); + }); + + const { + theme: { + colors: { accent_error, bg_user, black, text_low_emphasis }, + poll: { + createContent: { + pollOptions: { optionStyle }, + }, + }, + }, + } = useTheme(); + + return ( + + {hasDuplicate ? ( + + {t('This is already an option')} + + ) : null} + handleChangeText(newText, index)} + placeholder={t('Option')} + style={[styles.optionInput, { color: black }, optionStyle.input]} + value={option.text} + /> + + + + + + + ); +}; + +const MemoizedCreatePollOption = React.memo(CreatePollOption); + +export const CreatePollOptions = (props: { + currentOptionPositions: SharedValue; + duplicates: string[]; + pollOptions: PollOptionData[]; + setPollOptions: Dispatch>; +}) => { + const { t } = useTranslationContext(); + const { createPollOptionHeight = OPTION_HEIGHT } = useCreatePollContentContext(); + const { currentOptionPositions, duplicates = [], pollOptions, setPollOptions } = props; + const updateOption = useCallback( + (newText: string, index: number) => { + setPollOptions((prevOptions) => + prevOptions.map((option, idx) => (idx === index ? { ...option, text: newText } : option)), + ); + }, + [setPollOptions], + ); + + // used to know if drag is happening or not + const isDragging = useSharedValue<0 | 1>(0); + // this will hold id for item which user started dragging + const draggedItemId = useSharedValue(null); + + const boundaries = useMemo( + () => ({ maxBound: (pollOptions.length - 1) * createPollOptionHeight, minBound: 0 }), + [createPollOptionHeight, pollOptions.length], + ); + + const { + theme: { + colors: { bg_user, black }, + poll: { + createContent: { + pollOptions: { addOption, container, title }, + }, + }, + }, + } = useTheme(); + + return ( + + {t('Options')} + + {pollOptions.map((option, index) => ( + + ))} + + { + const newIndex = pollOptions.length; + currentOptionPositions.value = { + inverseIndexCache: { + ...currentOptionPositions.value.inverseIndexCache, + [newIndex]: newIndex, + }, + positionCache: { + ...currentOptionPositions.value.positionCache, + [newIndex]: { + updatedIndex: newIndex, + updatedTop: newIndex * createPollOptionHeight, + }, + }, + }; + setPollOptions([...pollOptions, { text: '' }]); + }} + style={[styles.addOptionWrapper, { backgroundColor: bg_user }, addOption.wrapper]} + > + + {t('Add an option')} + + + + ); +}; + +const styles = StyleSheet.create({ + addOptionWrapper: { + borderRadius: 12, + marginTop: 8, + paddingHorizontal: 16, + paddingVertical: 18, + }, + container: { marginVertical: 16 }, + optionInput: { + flex: 1, + fontSize: 16, + paddingRight: 4, + paddingVertical: 0, // android is adding extra padding so we remove it + }, + optionValidationError: { fontSize: 12, left: 16, position: 'absolute', top: 4 }, + optionWrapper: { + alignItems: 'center', + borderRadius: 12, + borderWidth: 1, + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 8, + paddingHorizontal: 16, + paddingVertical: 18, + }, + text: { fontSize: 16 }, +}); diff --git a/package/src/components/Poll/components/PollAnswersList.tsx b/package/src/components/Poll/components/PollAnswersList.tsx new file mode 100644 index 0000000000..2afa7581f2 --- /dev/null +++ b/package/src/components/Poll/components/PollAnswersList.tsx @@ -0,0 +1,134 @@ +import React, { useMemo } from 'react'; +import { FlatList, type FlatListProps, StyleSheet, Text, View } from 'react-native'; + +import { PollAnswer, VotingVisibility } from 'stream-chat'; + +import { AnswerListAddCommentButton } from './Button'; + +import { + PollContextProvider, + PollContextValue, + useTheme, + useTranslationContext, +} from '../../../contexts'; +import { DefaultStreamChatGenerics } from '../../../types/types'; +import { getDateString } from '../../../utils/i18n/getDateString'; +import { Avatar } from '../../Avatar/Avatar'; +import { usePollAnswersPagination } from '../hooks/usePollAnswersPagination'; +import { usePollState } from '../hooks/usePollState'; + +export type PollAnswersListProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = PollContextValue & { + additionalFlatListProps?: Partial>>; + PollAnswersListContent?: React.ComponentType; +}; + +export const PollAnswerListItem = ({ answer }: { answer: PollAnswer }) => { + const { t, tDateTimeParser } = useTranslationContext(); + const { voting_visibility } = usePollState(); + + const { + theme: { + colors: { bg_user, black }, + poll: { + answersList: { item: itemStyle }, + }, + }, + } = useTheme(); + + const dateString = useMemo( + () => + getDateString({ + date: answer.updated_at, + t, + tDateTimeParser, + timestampTranslationKey: 'timestamp/PollVote', + }), + [answer.updated_at, t, tDateTimeParser], + ); + + const isAnonymous = useMemo( + () => voting_visibility === VotingVisibility.anonymous, + [voting_visibility], + ); + + return ( + + + {answer.answer_text} + + + + {!isAnonymous && answer.user?.image ? ( + + ) : null} + + {isAnonymous ? t('Anonymous') : answer.user?.name} + + + {dateString} + + + ); +}; + +const PollAnswerListItemComponent = ({ item }: { item: PollAnswer }) => ( + +); + +export const PollAnswersListContent = ({ + additionalFlatListProps, +}: Pick) => { + const { hasNextPage, loadMore, pollAnswers } = usePollAnswersPagination(); + const { + theme: { + colors: { white }, + poll: { + answersList: { container }, + }, + }, + } = useTheme(); + + return ( + + `poll_answer_${item.id}`} + onEndReached={() => hasNextPage && loadMore()} + renderItem={PollAnswerListItemComponent} + {...additionalFlatListProps} + /> + + + ); +}; + +export const PollAnswersList = ({ + additionalFlatListProps, + message, + poll, + PollAnswersListContent: PollAnswersListOverride, +}: PollAnswersListProps) => ( + + {PollAnswersListOverride ? ( + + ) : ( + + )} + +); + +const styles = StyleSheet.create({ + container: { flex: 1, margin: 16 }, + listItemAnswerText: { fontSize: 16, fontWeight: '500' }, + listItemContainer: { + borderRadius: 12, + marginBottom: 8, + paddingBottom: 20, + paddingHorizontal: 16, + paddingTop: 12, + }, + listItemInfoContainer: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 24 }, + listItemUserInfoContainer: { alignItems: 'center', flexDirection: 'row' }, +}); diff --git a/package/src/components/Poll/components/PollInputDialog.tsx b/package/src/components/Poll/components/PollInputDialog.tsx new file mode 100644 index 0000000000..07036f6c51 --- /dev/null +++ b/package/src/components/Poll/components/PollInputDialog.tsx @@ -0,0 +1,117 @@ +import React, { useState } from 'react'; +import { + KeyboardAvoidingView, + Modal, + Platform, + StyleSheet, + Text, + TextInput, + TouchableOpacity, + View, +} from 'react-native'; + +import { useTheme, useTranslationContext } from '../../../contexts'; + +export type PollInputDialogProps = { + closeDialog: () => void; + onSubmit: (text: string) => void; + title: string; + visible: boolean; + initialValue?: string; +}; + +export const PollInputDialog = ({ + closeDialog, + initialValue = '', + onSubmit, + title, + visible, +}: PollInputDialogProps) => { + const { t } = useTranslationContext(); + const [dialogInput, setDialogInput] = useState(initialValue); + + const { + theme: { + colors: { accent_dark_blue, black, white }, + poll: { + inputDialog: { + button, + buttonContainer, + container, + input, + title: titleStyle, + transparentContainer, + }, + }, + }, + } = useTheme(); + + return ( + + + + {title} + ('Ask a question')} + style={[styles.input, { color: black }, input]} + value={dialogInput} + /> + + + + {t('Cancel')} + + + { + onSubmit(dialogInput); + closeDialog(); + }} + style={{ marginLeft: 32 }} + > + + {t('SEND')} + + + + + + + ); +}; + +const styles = StyleSheet.create({ + button: { fontSize: 16, fontWeight: '500' }, + buttonContainer: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: 52 }, + container: { + backgroundColor: 'white', + borderRadius: 16, + paddingBottom: 20, + paddingHorizontal: 16, + paddingTop: 32, + width: '80%', + }, + input: { + alignItems: 'center', + borderColor: 'gray', + borderRadius: 18, + borderWidth: 1, + fontSize: 16, + height: 36, + marginTop: 16, + padding: 0, + paddingHorizontal: 16, + }, + title: { fontSize: 17, fontWeight: '500', lineHeight: 20 }, + transparentContainer: { + alignItems: 'center', + backgroundColor: 'rgba(0, 0, 0, 0.4)', + flex: 1, + justifyContent: 'center', + }, +}); diff --git a/package/src/components/Poll/components/PollModalHeader.tsx b/package/src/components/Poll/components/PollModalHeader.tsx new file mode 100644 index 0000000000..5f35460b93 --- /dev/null +++ b/package/src/components/Poll/components/PollModalHeader.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; + +import { useTheme } from '../../../contexts'; +import { Back } from '../../../icons'; + +export type PollModalHeaderProps = { + onPress: () => void; + title: string; +}; + +export const PollModalHeader = ({ onPress, title }: PollModalHeaderProps) => { + const { + theme: { + colors: { black, text_high_emphasis, white }, + poll: { + modalHeader: { container, title: titleStyle }, + }, + }, + } = useTheme(); + + return ( + + + + + + {title} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + alignItems: 'center', + flexDirection: 'row', + paddingHorizontal: 16, + paddingVertical: 18, + }, + title: { fontSize: 16, fontWeight: '500', marginLeft: 32 }, +}); diff --git a/package/src/components/Poll/components/PollOption.tsx b/package/src/components/Poll/components/PollOption.tsx new file mode 100644 index 0000000000..69a22c2b82 --- /dev/null +++ b/package/src/components/Poll/components/PollOption.tsx @@ -0,0 +1,172 @@ +import React, { useMemo } from 'react'; + +import { ScrollViewProps, StyleSheet, Text, View } from 'react-native'; + +import { ScrollView } from 'react-native-gesture-handler'; + +import { PollOption as PollOptionClass, PollVote } from 'stream-chat'; + +import { VoteButton } from './Button'; + +import { PollContextProvider, PollContextValue, useTheme } from '../../../contexts'; + +import { Avatar } from '../../Avatar/Avatar'; +import { usePollState } from '../hooks/usePollState'; + +export type PollOptionProps = { + option: PollOptionClass; + showProgressBar?: boolean; +}; + +export type PollAllOptionsContentProps = PollContextValue & { + additionalScrollViewProps?: Partial; + PollAllOptionsContent?: React.ComponentType; +}; + +export const PollAllOptionsContent = ({ + additionalScrollViewProps, +}: Pick) => { + const { name, options } = usePollState(); + + const { + theme: { + colors: { bg_user, black, white }, + poll: { + allOptions: { listContainer, titleContainer, titleText, wrapper }, + }, + }, + } = useTheme(); + + return ( + + + {name} + + + {options?.map((option: PollOptionClass) => ( + + + + ))} + + + ); +}; + +export const PollAllOptions = ({ + additionalScrollViewProps, + message, + poll, + PollAllOptionsContent: PollAllOptionsContentOverride, +}: PollAllOptionsContentProps) => ( + + {PollAllOptionsContentOverride ? ( + + ) : ( + + )} + +); + +export const PollOption = ({ option, showProgressBar = true }: PollOptionProps) => { + const { is_closed, latest_votes_by_option, maxVotedOptionIds, vote_counts_by_option } = + usePollState(); + + const relevantVotes = useMemo( + () => latest_votes_by_option?.[option.id]?.slice(0, 2) || [], + [latest_votes_by_option, option.id], + ); + const maxVotes = useMemo( + () => + maxVotedOptionIds?.[0] && vote_counts_by_option + ? vote_counts_by_option[maxVotedOptionIds[0]] + : 0, + [maxVotedOptionIds, vote_counts_by_option], + ); + const votes = vote_counts_by_option[option.id] || 0; + + const { + theme: { + colors: { accent_dark_blue, accent_info, black, grey }, + poll: { + message: { + option: { + container, + progressBar, + progressBarEmptyFill, + progressBarVotedFill, + progressBarWinnerFill, + text, + votesContainer, + wrapper, + }, + }, + }, + }, + } = useTheme(); + + return ( + + + + {option.text} + + {relevantVotes.map((vote: PollVote) => ( + + ))} + + {vote_counts_by_option[option.id] || 0} + + + + {showProgressBar ? ( + + 0 ? votes / maxVotes : 0, + }} + /> + 0 ? (maxVotes - votes) / maxVotes : 1, + }} + /> + + ) : null} + + ); +}; + +const styles = StyleSheet.create({ + allOptionsListContainer: { + borderRadius: 12, + marginTop: 32, + paddingBottom: 18, + paddingHorizontal: 16, + }, + allOptionsTitleContainer: { + borderRadius: 12, + paddingHorizontal: 16, + paddingVertical: 18, + }, + allOptionsTitleText: { fontSize: 16, fontWeight: '500' }, + allOptionsWrapper: { flex: 1, marginBottom: 16, padding: 16 }, + container: { flexDirection: 'row' }, + progressBar: { borderRadius: 4, flex: 1, flexDirection: 'row', height: 4, marginTop: 2 }, + text: { + flex: 1, + fontSize: 16, + marginLeft: 4, + }, + votesContainer: { flexDirection: 'row', marginLeft: 4 }, + wrapper: { marginTop: 8, paddingVertical: 8 }, +}); diff --git a/package/src/components/Poll/components/PollResults/PollOptionFullResults.tsx b/package/src/components/Poll/components/PollResults/PollOptionFullResults.tsx new file mode 100644 index 0000000000..84bf745075 --- /dev/null +++ b/package/src/components/Poll/components/PollResults/PollOptionFullResults.tsx @@ -0,0 +1,108 @@ +import React, { useCallback } from 'react'; +import { FlatList, type FlatListProps, StyleSheet, Text, View } from 'react-native'; + +import { PollOption, PollVote as PollVoteClass } from 'stream-chat'; + +import { PollVote } from './PollResultItem'; + +import { + PollContextProvider, + PollContextValue, + useTheme, + useTranslationContext, +} from '../../../../contexts'; +import type { DefaultStreamChatGenerics } from '../../../../types/types'; +import { usePollOptionVotesPagination } from '../../hooks/usePollOptionVotesPagination'; +import { usePollState } from '../../hooks/usePollState'; + +export type PollOptionFullResultsProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = PollContextValue & { + option: PollOption; + additionalFlatListProps?: Partial>>; + PollOptionFullResultsContent?: React.ComponentType<{ option: PollOption }>; +}; + +export const PollOptionFullResultsItem = ({ item }: { item: PollVoteClass }) => ( + +); + +export const PollOptionFullResultsContent = ({ + additionalFlatListProps, + option, +}: Pick) => { + const { t } = useTranslationContext(); + const { hasNextPage, loadMore, votes } = usePollOptionVotesPagination({ option }); + const { vote_counts_by_option } = usePollState(); + + const { + theme: { + colors: { bg_user, black, white }, + poll: { + fullResults: { container, contentContainer, headerContainer, headerText }, + }, + }, + } = useTheme(); + + const PollOptionFullResultsHeader = useCallback( + () => ( + + + {t('{{count}} votes', { count: vote_counts_by_option[option.id] ?? 0 })} + + + ), + [black, headerContainer, headerText, option.id, t, vote_counts_by_option], + ); + + return ( + + `option_full_results_${item.id}`} + ListHeaderComponent={PollOptionFullResultsHeader} + onEndReached={() => hasNextPage && loadMore()} + renderItem={PollOptionFullResultsItem} + {...additionalFlatListProps} + /> + + ); +}; + +export const PollOptionFullResults = ({ + additionalFlatListProps, + message, + option, + poll, + PollOptionFullResultsContent: PollOptionFullResultsContentOverride, +}: PollOptionFullResultsProps) => ( + + {PollOptionFullResultsContentOverride ? ( + + ) : ( + + )} + +); + +const styles = StyleSheet.create({ + container: { flex: 1 }, + contentContainer: { + borderRadius: 12, + marginBottom: 8, + marginHorizontal: 16, + marginTop: 16, + paddingHorizontal: 16, + paddingVertical: 12, + }, + headerContainer: { flexDirection: 'row', justifyContent: 'flex-end', marginBottom: 8 }, + headerText: { fontSize: 16, marginLeft: 16 }, +}); diff --git a/package/src/components/Poll/components/PollResults/PollResultItem.tsx b/package/src/components/Poll/components/PollResults/PollResultItem.tsx new file mode 100644 index 0000000000..f3d4ad9e36 --- /dev/null +++ b/package/src/components/Poll/components/PollResults/PollResultItem.tsx @@ -0,0 +1,115 @@ +import React, { useMemo } from 'react'; +import { StyleSheet, Text, View } from 'react-native'; + +import { PollOption, PollVote as PollVoteClass, VotingVisibility } from 'stream-chat'; + +import { useTheme, useTranslationContext } from '../../../../contexts'; +import type { DefaultStreamChatGenerics } from '../../../../types/types'; +import { getDateString } from '../../../../utils/i18n/getDateString'; +import { Avatar } from '../../../Avatar/Avatar'; +import { usePollState } from '../../hooks/usePollState'; +import { ShowAllVotesButton } from '../Button'; + +export type PollResultItemProps< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = { + option: PollOption; +}; + +export const PollVote = (vote: PollVoteClass) => { + const { t, tDateTimeParser } = useTranslationContext(); + const { voting_visibility } = usePollState(); + const { + theme: { + colors: { black, text_low_emphasis }, + poll: { + results: { + vote: { container, dateText, userName }, + }, + }, + }, + } = useTheme(); + + const dateString = useMemo( + () => + getDateString({ + date: vote.created_at, + t, + tDateTimeParser, + timestampTranslationKey: 'timestamp/PollVote', + }), + [vote.created_at, t, tDateTimeParser], + ); + + const isAnonymous = useMemo( + () => voting_visibility === VotingVisibility.anonymous, + [voting_visibility], + ); + + return ( + + + {!isAnonymous && vote.user?.image ? ( + + ) : null} + + {isAnonymous ? t('Anonymous') : vote.user?.name} + + + {dateString} + + ); +}; + +export const PollResultsItem = ({ option }: PollResultItemProps) => { + const { t } = useTranslationContext(); + const { latest_votes_by_option, vote_counts_by_option } = usePollState(); + + const { + theme: { + colors: { bg_user, black }, + poll: { + results: { + item: { container, headerContainer, title, voteCount }, + }, + }, + }, + } = useTheme(); + + return ( + + + {option.text} + + {t('{{count}} votes', { count: vote_counts_by_option[option.id] ?? 0 })} + + + {latest_votes_by_option?.[option.id]?.length > 0 ? ( + + {(latest_votes_by_option?.[option.id] ?? []).slice(0, 5).map(PollVote)} + + ) : null} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + borderRadius: 12, + marginBottom: 8, + paddingHorizontal: 16, + paddingVertical: 12, + }, + headerContainer: { flexDirection: 'row', justifyContent: 'space-between' }, + title: { flex: 1, fontSize: 16, fontWeight: '500' }, + voteContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + marginBottom: 8, + paddingVertical: 8, + }, + voteCount: { fontSize: 16, marginLeft: 16 }, + voteDate: { fontSize: 14 }, + voteUserName: { fontSize: 14, marginLeft: 2 }, +}); diff --git a/package/src/components/Poll/components/PollResults/PollResults.tsx b/package/src/components/Poll/components/PollResults/PollResults.tsx new file mode 100644 index 0000000000..ee107f5f0f --- /dev/null +++ b/package/src/components/Poll/components/PollResults/PollResults.tsx @@ -0,0 +1,79 @@ +import React, { useMemo } from 'react'; +import { ScrollViewProps, StyleSheet, Text, View } from 'react-native'; + +import { ScrollView } from 'react-native-gesture-handler'; + +import { PollResultsItem } from './PollResultItem'; + +import { PollContextProvider, PollContextValue, useTheme } from '../../../../contexts'; +import { usePollState } from '../../hooks/usePollState'; + +export type PollResultsProps = PollContextValue & { + additionalScrollViewProps?: Partial; + PollResultsContent?: React.ComponentType; +}; + +export const PollResultsContent = ({ + additionalScrollViewProps, +}: Pick) => { + const { name, options, vote_counts_by_option } = usePollState(); + + const sortedOptions = useMemo( + () => + [...options].sort( + (a, b) => (vote_counts_by_option[b.id] ?? 0) - (vote_counts_by_option[a.id] ?? 0), + ), + [vote_counts_by_option, options], + ); + + const { + theme: { + colors: { bg_user, black, white }, + poll: { + results: { container, scrollView, title }, + }, + }, + } = useTheme(); + + return ( + + + {name} + + + {sortedOptions.map((option) => ( + + ))} + + + ); +}; + +export const PollResults = ({ + additionalScrollViewProps, + message, + poll, + PollResultsContent: PollResultsContentOverride, +}: PollResultsProps) => ( + + {PollResultsContentOverride ? ( + + ) : ( + + )} + +); + +const styles = StyleSheet.create({ + container: { + borderRadius: 12, + marginTop: 16, + paddingHorizontal: 16, + paddingVertical: 18, + }, + scrollView: { flex: 1, marginHorizontal: 16 }, + title: { fontSize: 16, fontWeight: '500' }, +}); diff --git a/package/src/components/Poll/components/PollResults/index.ts b/package/src/components/Poll/components/PollResults/index.ts new file mode 100644 index 0000000000..41158dccb8 --- /dev/null +++ b/package/src/components/Poll/components/PollResults/index.ts @@ -0,0 +1,3 @@ +export * from './PollResults'; +export * from './PollResultItem'; +export * from './PollOptionFullResults'; diff --git a/package/src/components/Poll/components/index.ts b/package/src/components/Poll/components/index.ts new file mode 100644 index 0000000000..874aba5125 --- /dev/null +++ b/package/src/components/Poll/components/index.ts @@ -0,0 +1,8 @@ +export * from './Button'; +export * from './CreatePollIcon'; +export * from './CreatePollOptions'; +export * from './PollAnswersList'; +export * from './PollInputDialog'; +export * from './PollOption'; +export * from './PollResults'; +export * from './PollModalHeader'; diff --git a/package/src/components/Poll/hooks/usePollAnswersPagination.ts b/package/src/components/Poll/hooks/usePollAnswersPagination.ts new file mode 100644 index 0000000000..aab834a09a --- /dev/null +++ b/package/src/components/Poll/hooks/usePollAnswersPagination.ts @@ -0,0 +1,109 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +import uniqBy from 'lodash/uniqBy'; +import type { PollAnswer, PollAnswersQueryParams } from 'stream-chat'; +import { isVoteAnswer } from 'stream-chat'; + +import { useChatContext, usePollContext } from '../../../contexts'; + +export type UsePollAnswersPaginationParams = { + loadFirstPage?: boolean; + paginationParams?: PollAnswersQueryParams; +}; + +export type UsePollAnswersReturnType = { + error: Error | undefined; + hasNextPage: boolean; + loading: boolean; + loadMore: () => void; + next: string | null | undefined; + pollAnswers: PollAnswer[]; +}; + +/** + * A hook that queries answers for a given Poll and returns them in a paginated fashion. + * Should be used instead of the latest_answers property within the reactive state in the + * event that we need more than the top 10 answers. The returned property pollAnswers will + * automatically be updated and trigger a state change when paginating further. + * + * @param loadFirstPage {boolean} Signifies whether the first page should be automatically loaded whenever + * the hook is first called. + * @param paginationParams {PollAnswersQueryParams} The pagination params we might want to use for our custom + * needs when invoking the hook. + * + * @returns {UsePollAnswersReturnType} An object containing all of the needed pagination values as well as the + * answers. + **/ +export const usePollAnswersPagination = ({ + loadFirstPage = true, + paginationParams, +}: UsePollAnswersPaginationParams = {}): UsePollAnswersReturnType => { + const { poll } = usePollContext(); + const { client } = useChatContext(); + + const [pollAnswers, setPollAnswers] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(); + const cursorRef = useRef(); + const queryInProgress = useRef(false); + + const loadMore = useCallback(async () => { + if (cursorRef.current === null || queryInProgress.current) return; + const next = cursorRef.current; + + setLoading(true); + queryInProgress.current = true; + try { + const { next: newNext, votes: answers } = await poll.queryAnswers({ + filter: paginationParams?.filter, + options: !next ? paginationParams?.options : { ...paginationParams?.options, next }, + sort: { updated_at: -1, ...paginationParams?.sort }, + }); + cursorRef.current = newNext || null; + setPollAnswers((prev) => uniqBy([...prev, ...answers], 'id')); + } catch (e) { + setError(e as Error); + } + queryInProgress.current = false; + setLoading(false); + }, [paginationParams, poll]); + + useEffect(() => { + if (!loadFirstPage || pollAnswers.length) return; + loadMore(); + }, [loadFirstPage, loadMore, pollAnswers]); + + useEffect(() => { + const castedListeners = ['poll.vote_casted', 'poll.vote_changed'].map((eventName) => + client.on(eventName, (event) => { + if (event.poll?.id && event.poll.id !== poll.id) return; + const vote = event.poll_vote; + if (vote && isVoteAnswer(vote)) { + setPollAnswers([vote, ...pollAnswers.filter((answer) => answer.id !== vote.id)]); + } + }), + ); + + const removedListener = client.on('poll.vote_removed', (event) => { + if (event.poll?.id && event.poll.id !== poll.id) return; + const vote = event.poll_vote; + if (vote && isVoteAnswer(vote)) { + setPollAnswers(pollAnswers.filter((item) => item.id !== vote.id)); + } + }); + + return () => { + castedListeners.forEach((listener) => listener.unsubscribe()); + removedListener.unsubscribe(); + }; + }, [client, poll, pollAnswers]); + + return { + error, + hasNextPage: cursorRef.current !== null, + loading, + loadMore, + next: cursorRef.current, + pollAnswers, + }; +}; diff --git a/package/src/components/Poll/hooks/usePollOptionVotesPagination.ts b/package/src/components/Poll/hooks/usePollOptionVotesPagination.ts new file mode 100644 index 0000000000..c5cd557f5e --- /dev/null +++ b/package/src/components/Poll/hooks/usePollOptionVotesPagination.ts @@ -0,0 +1,119 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import uniqBy from 'lodash/uniqBy'; +import { isVoteAnswer, PollOption, PollOptionVotesQueryParams, PollVote } from 'stream-chat'; + +import { useChatContext, usePollContext } from '../../../contexts'; + +export type UsePollOptionVotesPaginationParams = { + option: PollOption; + loadFirstPage?: boolean; + paginationParams?: PollOptionVotesQueryParams; +}; + +export type UsePollVotesReturnType = { + error: Error | undefined; + hasNextPage: boolean; + loading: boolean; + loadMore: () => void; + next: string | null | undefined; + votes: PollVote[]; +}; + +/** + * A hook that queries votes for a given Poll and returns them in a paginated fashion. + * Should be used instead of the latest_votes_by_option property within the reactive state in the + * event that we need more than the top 10 votes for an option. The returned property votes will + * automatically be updated and trigger a state change when paginating further. Querying for votes + * can only be done on an option by option basis. + * + * @param option {PollOption} The option for which we want to load the votes. + * @param loadFirstPage {boolean} Signifies whether the first page should be automatically loaded whenever + * the hook is first called. + * @param paginationParams {PollOptionVotesQueryParams} The pagination params we might want to use for our custom + * needs when invoking the hook. + * + * @returns {UsePollVotesReturnType} An object containing all of the needed pagination values as well as the + * answers. + **/ + +export const usePollOptionVotesPagination = ({ + loadFirstPage = true, + option, + paginationParams, +}: UsePollOptionVotesPaginationParams): UsePollVotesReturnType => { + const { poll } = usePollContext(); + const { client } = useChatContext(); + + const [votes, setVotes] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(); + const cursorRef = useRef(); + const queryInProgress = useRef(false); + const optionFilter = useMemo(() => ({ option_id: option.id }), [option.id]); + + const loadMore = useCallback(async () => { + if (cursorRef.current === null || queryInProgress.current) return; + const next = cursorRef.current; + + setLoading(true); + queryInProgress.current = true; + try { + const { next: newNext, votes } = await poll.queryOptionVotes({ + filter: { ...optionFilter, ...paginationParams?.filter }, + options: !next ? paginationParams?.options : { ...paginationParams?.options, next }, + sort: { created_at: -1, ...paginationParams?.sort }, + }); + cursorRef.current = newNext || null; + setVotes((prev) => uniqBy([...prev, ...votes], 'id')); + } catch (e) { + setError(e as Error); + } + queryInProgress.current = false; + setLoading(false); + }, [optionFilter, paginationParams, poll]); + + useEffect(() => { + if (!loadFirstPage || votes.length) return; + loadMore(); + }, [loadFirstPage, loadMore, votes]); + + // TODO: Possibly generalize these in a utility hook. + useEffect(() => { + const castedListeners = ['poll.vote_casted', 'poll.vote_changed'].map((eventName) => + client.on(eventName, (event) => { + if (event.poll?.id && event.poll.id !== poll.id) return; + const vote = event.poll_vote; + if (vote && !isVoteAnswer(vote)) { + if (vote.option_id === option.id) { + setVotes([vote, ...votes.filter((v) => v.id !== vote.id)]); + } else if (eventName === 'poll.vote_changed') { + setVotes(votes.filter((v) => v.id !== vote.id)); + } + } + }), + ); + + const removedListener = client.on('poll.vote_removed', (event) => { + if (event.poll?.id && event.poll.id !== poll.id) return; + const vote = event.poll_vote; + if (vote && !isVoteAnswer(vote) && vote.option_id === option.id) { + setVotes(votes.filter((v) => v.id !== vote.id)); + } + }); + + return () => { + castedListeners.forEach((listener) => listener.unsubscribe()); + removedListener.unsubscribe(); + }; + }, [client, option, poll, votes]); + + return { + error, + hasNextPage: cursorRef.current !== null, + loading, + loadMore, + next: cursorRef.current, + votes, + }; +}; diff --git a/package/src/components/Poll/hooks/usePollState.ts b/package/src/components/Poll/hooks/usePollState.ts new file mode 100644 index 0000000000..162a59fe4f --- /dev/null +++ b/package/src/components/Poll/hooks/usePollState.ts @@ -0,0 +1,122 @@ +import { useCallback } from 'react'; + +import { + APIResponse, + CastVoteAPIResponse, + PollAnswer, + PollOption, + PollState, + PollVote, + UpdatePollAPIResponse, + UserResponse, + VotingVisibility, +} from 'stream-chat'; + +import { usePollStateStore } from './usePollStateStore'; + +import { usePollContext } from '../../../contexts'; + +import { DefaultStreamChatGenerics } from '../../../types/types'; + +export type UsePollStateSelectorReturnType = { + allow_answers: boolean | undefined; + allow_user_suggested_options: boolean | undefined; + answers_count: number; + created_by: UserResponse | null; + enforce_unique_vote: boolean; + is_closed: boolean | undefined; + latest_votes_by_option: Record; + max_votes_allowed: number; + maxVotedOptionIds: string[]; + name: string; + options: PollOption[]; + ownAnswer: PollAnswer | undefined; + ownVotesByOptionId: Record; + vote_counts_by_option: Record; + voting_visibility: VotingVisibility | undefined; +}; + +export type UsePollStateReturnType< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = UsePollStateSelectorReturnType & { + addComment: ( + answerText: string, + ) => Promise>; + addOption: (optionText: string) => Promise; + endVote: () => Promise>; +}; + +const selector = ( + nextValue: PollState, +): UsePollStateSelectorReturnType => ({ + allow_answers: nextValue.allow_answers, + allow_user_suggested_options: nextValue.allow_user_suggested_options, + answers_count: nextValue.answers_count, + created_by: nextValue.created_by, + enforce_unique_vote: nextValue.enforce_unique_vote, + is_closed: nextValue.is_closed, + latest_votes_by_option: nextValue.latest_votes_by_option, + max_votes_allowed: nextValue.max_votes_allowed, + maxVotedOptionIds: nextValue.maxVotedOptionIds, + name: nextValue.name, + options: nextValue.options, + ownAnswer: nextValue.ownAnswer, + ownVotesByOptionId: nextValue.ownVotesByOptionId, + vote_counts_by_option: nextValue.vote_counts_by_option, + voting_visibility: nextValue.voting_visibility, +}); + +export const usePollState = (): UsePollStateReturnType => { + const { message, poll } = usePollContext(); + const { + allow_answers, + allow_user_suggested_options, + answers_count, + created_by, + enforce_unique_vote, + is_closed, + latest_votes_by_option, + max_votes_allowed, + maxVotedOptionIds, + name, + options, + ownAnswer, + ownVotesByOptionId, + vote_counts_by_option, + voting_visibility, + } = usePollStateStore(selector); + + const addOption = useCallback( + async (optionText: string) => { + const { poll_option } = await poll.createOption({ text: optionText }); + await poll.castVote(poll_option.id, message.id); + }, + [message, poll], + ); + const addComment = useCallback( + (answerText: string) => poll.addAnswer(answerText, message.id), + [message.id, poll], + ); + const endVote = useCallback(() => poll.close(), [poll]); + + return { + addComment, + addOption, + allow_answers, + allow_user_suggested_options, + answers_count, + created_by, + endVote, + enforce_unique_vote, + is_closed, + latest_votes_by_option, + max_votes_allowed, + maxVotedOptionIds, + name, + options, + ownAnswer, + ownVotesByOptionId, + vote_counts_by_option, + voting_visibility, + }; +}; diff --git a/package/src/components/Poll/hooks/usePollStateStore.ts b/package/src/components/Poll/hooks/usePollStateStore.ts new file mode 100644 index 0000000000..bbfc6e7e9e --- /dev/null +++ b/package/src/components/Poll/hooks/usePollStateStore.ts @@ -0,0 +1,13 @@ +import { PollState } from 'stream-chat'; + +import { usePollContext } from '../../../contexts'; +import { useStateStore } from '../../../hooks'; + +export const usePollStateStore = < + T extends Readonly | Readonly>, +>( + selector: (nextValue: PollState) => T, +): T => { + const { poll } = usePollContext(); + return useStateStore(poll.state, selector); +}; diff --git a/package/src/components/Poll/index.ts b/package/src/components/Poll/index.ts new file mode 100644 index 0000000000..ed6e2af135 --- /dev/null +++ b/package/src/components/Poll/index.ts @@ -0,0 +1,10 @@ +// components +export * from './Poll'; +export * from './CreatePollContent'; +export * from './components'; + +// hooks +export * from './hooks/usePollState'; +export * from './hooks/usePollStateStore'; +export * from './hooks/usePollAnswersPagination'; +export * from './hooks/usePollOptionVotesPagination'; diff --git a/package/src/components/Reply/Reply.tsx b/package/src/components/Reply/Reply.tsx index c0207db88f..83ead74468 100644 --- a/package/src/components/Reply/Reply.tsx +++ b/package/src/components/Reply/Reply.tsx @@ -262,6 +262,8 @@ const ReplyWithContext = < text: quotedMessage.type === 'deleted' ? `_${t('Message deleted')}_` + : quotedMessage.poll + ? `📊 ${quotedMessage.poll.name}` : quotedMessage.text ? quotedMessage.text.length > 170 ? `${quotedMessage.text.slice(0, 170)}...` diff --git a/package/src/components/index.ts b/package/src/components/index.ts index 19777e0107..afc2e4e6b0 100644 --- a/package/src/components/index.ts +++ b/package/src/components/index.ts @@ -161,6 +161,7 @@ export * from './MessageOverlay/OverlayReactionsAvatar'; export * from './MessageOverlay/OverlayReactionList'; export * from './ProgressControl/ProgressControl'; +export * from './Poll'; export * from './Reply/Reply'; diff --git a/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx b/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx index d89de3b6da..f4d48aa115 100644 --- a/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx +++ b/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx @@ -59,6 +59,12 @@ export type AttachmentPickerContextValue = { */ CameraSelectorIcon: React.ComponentType; closePicker: () => void; + /** + * Custom UI component for the poll creation icon. + * + * **Default: ** [CreatePollIcon](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/AttachmentPicker/components/CreatePollIcon.tsx) + */ + CreatePollIcon: React.ComponentType; /** * Custom UI component for [file selector icon](https://github.com/GetStream/stream-chat-react-native/blob/main/screenshots/docs/1.png) * @@ -98,7 +104,12 @@ export const AttachmentPickerProvider = ({ }: PropsWithChildren<{ value?: Pick< AttachmentPickerContextValue, - 'CameraSelectorIcon' | 'closePicker' | 'FileSelectorIcon' | 'ImageSelectorIcon' | 'openPicker' + | 'CameraSelectorIcon' + | 'closePicker' + | 'CreatePollIcon' + | 'FileSelectorIcon' + | 'ImageSelectorIcon' + | 'openPicker' > & Partial>; }>) => { diff --git a/package/src/contexts/index.ts b/package/src/contexts/index.ts index 69f0f2260d..130467fd67 100644 --- a/package/src/contexts/index.ts +++ b/package/src/contexts/index.ts @@ -24,3 +24,4 @@ export * from './threadsContext/ThreadListItemContext'; export * from './translationContext/TranslationContext'; export * from './typingContext/TypingContext'; export * from './utils/getDisplayName'; +export * from './pollContext'; diff --git a/package/src/contexts/messageInputContext/MessageInputContext.tsx b/package/src/contexts/messageInputContext/MessageInputContext.tsx index 1069c4031d..b243c1fca9 100644 --- a/package/src/contexts/messageInputContext/MessageInputContext.tsx +++ b/package/src/contexts/messageInputContext/MessageInputContext.tsx @@ -29,6 +29,7 @@ import { useMessageDetailsForState } from './hooks/useMessageDetailsForState'; import { isUploadAllowed, MAX_FILE_SIZE_TO_UPLOAD, prettifyFileSize } from './utils/utils'; +import { PollContentProps } from '../../components'; import { AudioAttachmentProps } from '../../components/Attachment/AudioAttachment'; import { parseLinksFromText } from '../../components/Message/MessageSimple/utils/parseLinks'; import type { AttachButtonProps } from '../../components/MessageInput/AttachButton'; @@ -326,7 +327,6 @@ export type InputMessageInputContextValue< */ CooldownTimer: React.ComponentType; editMessage: StreamChat['updateMessage']; - /** * Custom UI component for FileUploadPreview. * Defaults to and accepts same props as: https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageInput/FileUploadPreview.tsx @@ -335,6 +335,7 @@ export type InputMessageInputContextValue< /** When false, CameraSelectorIcon will be hidden */ hasCameraPicker: boolean; + /** When false, CommandsButton will be hidden */ hasCommands: boolean; /** When false, FileSelectorIcon will be hidden */ @@ -351,13 +352,13 @@ export type InputMessageInputContextValue< InputReplyStateHeader: React.ComponentType>; /** Limit on allowed number of files to attach at a time. */ maxNumberOfFiles: number; - /** * Custom UI component for more options button. * * Defaults to and accepts same props as: [MoreOptionsButton](https://getstream.io/chat/docs/sdk/reactnative/ui-components/more-options-button/) */ MoreOptionsButton: React.ComponentType; + /** Limit on the number of lines in the text input before scrolling */ numberOfLines: number; quotedMessage: boolean | MessageType; @@ -377,6 +378,7 @@ export type InputMessageInputContextValue< ShowThreadMessageInChannelButton: React.ComponentType<{ threadList?: boolean; }>; + /** * Custom UI component for audio recording mic button. * @@ -389,7 +391,6 @@ export type InputMessageInputContextValue< * **Default** [UploadProgressIndicator](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/MessageInput/UploadProgressIndicator.tsx) */ UploadProgressIndicator: React.ComponentType; - /** * Additional props for underlying TextInput component. These props will be forwarded as it is to TextInput component. * @@ -404,6 +405,7 @@ export type InputMessageInputContextValue< autoCompleteTriggerSettings?: ( settings: ACITriggerSettingsParams, ) => TriggerSettings; + closePollCreationDialog?: () => void; /** * Compress image with quality (from 0 to 1, where 1 is best quality). * On iOS, values larger than 0.8 don't produce a noticeable quality increase in most images, @@ -411,6 +413,13 @@ export type InputMessageInputContextValue< * Image picker defaults to 0.8 for iOS and 1 for Android */ compressImageQuality?: number; + + /** + * Override the entire content of the CreatePoll component. The component has full access to the + * useCreatePollContext() hook. + * */ + CreatePollContent?: React.ComponentType; + /** * Override file upload request * @@ -423,6 +432,7 @@ export type InputMessageInputContextValue< file: File, channel: ChannelContextValue['channel'], ) => Promise; + /** * Override image upload request * @@ -444,17 +454,14 @@ export type InputMessageInputContextValue< * It is defined with message type if the editing state is true, else its undefined. */ editing?: MessageType; - /** * Prop to override the default emoji search index in auto complete suggestion list. */ emojiSearchIndex?: EmojiSearchIndex; - /** * Handler for when the attach button is pressed. */ handleAttachButtonPress?: () => void; - /** Initial value to set on input */ initialValue?: string; /** @@ -484,13 +491,14 @@ export type InputMessageInputContextValue< */ InputButtons?: React.ComponentType>; maxMessageLength?: number; - mentionAllAppUsersEnabled?: boolean; /** Object containing filters/sort/options overrides for an @mention user query */ + mentionAllAppUsersEnabled?: boolean; mentionAllAppUsersQuery?: MentionAllAppUsersQuery; /** * Callback that is called when the text input's text changes. Changed text is passed as a single string argument to the callback handler. */ onChangeText?: (newText: string) => void; + openPollCreationDialog?: ({ sendMessage }: Pick) => void; SendMessageDisallowedIndicator?: React.ComponentType; /** * ref for input setter function @@ -500,6 +508,7 @@ export type InputMessageInputContextValue< * @overrideType Function */ setInputRef?: (ref: TextInput | null) => void; + showPollCreationDialog?: boolean; }; export type MessageInputContextValue< @@ -567,7 +576,16 @@ export const MessageInputProvider = < }>({}); const [giphyActive, setGiphyActive] = useState(false); const [sendThreadMessageInChannel, setSendThreadMessageInChannel] = useState(false); - const { editing, initialValue } = value; + const [showPollCreationDialog, setShowPollCreationDialog] = useState(false); + + const defaultOpenPollCreationDialog = useCallback(() => setShowPollCreationDialog(true), []); + const closePollCreationDialog = useCallback(() => setShowPollCreationDialog(false), []); + + const { + editing, + initialValue, + openPollCreationDialog: openPollCreationDialogFromContext, + } = value; const { fileUploads, imageUploads, @@ -978,7 +996,7 @@ export const MessageInputProvider = < } // Disallow sending message if its empty. - if (!prevText && attachments.length === 0) { + if (!prevText && attachments.length === 0 && !customMessageData?.poll_id) { sending.current = false; return; } @@ -1399,6 +1417,14 @@ export const MessageInputProvider = < } }; + const openPollCreationDialog = () => { + if (openPollCreationDialogFromContext) { + openPollCreationDialogFromContext({ sendMessage }); + return; + } + defaultOpenPollCreationDialog(); + }; + const messageInputContext = useCreateMessageInputContext({ appendText, asyncIds, @@ -1450,9 +1476,11 @@ export const MessageInputProvider = < uploadNewFile, uploadNewImage, ...value, + closePollCreationDialog, + openPollCreationDialog, sendMessage, // overriding the originally passed in sendMessage + showPollCreationDialog, }); - return ( ); -afterEach(jest.clearAllMocks); - describe("MessageInputContext's pickFile", () => { + afterEach(jest.clearAllMocks); jest.spyOn(Alert, 'alert'); jest.spyOn(NativeUtils, 'pickDocument').mockImplementation( jest.fn().mockResolvedValue({ @@ -109,5 +108,8 @@ describe("MessageInputContext's pickFile", () => { }); expect(Alert.alert).toHaveBeenCalledTimes(2); + expect(Alert.alert).toHaveBeenCalledWith( + 'File is too large: {{ size }}, maximum upload size is {{ limit }}', + ); }); }); diff --git a/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts b/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts index d27fcd9089..8cbcac1872 100644 --- a/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts +++ b/package/src/contexts/messageInputContext/hooks/useCreateMessageInputContext.ts @@ -27,10 +27,12 @@ export const useCreateMessageInputContext = < clearEditingState, clearQuotedMessageState, closeAttachmentPicker, + closePollCreationDialog, CommandsButton, compressImageQuality, cooldownEndsAt, CooldownTimer, + CreatePollContent, doDocUploadRequest, doImageUploadRequest, editing, @@ -69,6 +71,7 @@ export const useCreateMessageInputContext = < openCommandsPicker, openFilePicker, openMentionsPicker, + openPollCreationDialog, pickAndUploadImageFromNativePicker, pickFile, quotedMessage, @@ -97,6 +100,7 @@ export const useCreateMessageInputContext = < setShowMoreOptions, setText, showMoreOptions, + showPollCreationDialog, ShowThreadMessageInChannelButton, StartAudioRecordingButton, takeAndUploadImage, @@ -149,10 +153,12 @@ export const useCreateMessageInputContext = < clearEditingState, clearQuotedMessageState, closeAttachmentPicker, + closePollCreationDialog, CommandsButton, compressImageQuality, cooldownEndsAt, CooldownTimer, + CreatePollContent, doDocUploadRequest, doImageUploadRequest, editing, @@ -191,6 +197,7 @@ export const useCreateMessageInputContext = < openCommandsPicker, openFilePicker, openMentionsPicker, + openPollCreationDialog, pickAndUploadImageFromNativePicker, pickFile, quotedMessage, @@ -219,6 +226,7 @@ export const useCreateMessageInputContext = < setShowMoreOptions, setText, showMoreOptions, + showPollCreationDialog, ShowThreadMessageInChannelButton, StartAudioRecordingButton, takeAndUploadImage, @@ -249,6 +257,7 @@ export const useCreateMessageInputContext = < showMoreOptions, text, threadId, + showPollCreationDialog, ], ); diff --git a/package/src/contexts/messageOverlayContext/MessageOverlayContext.tsx b/package/src/contexts/messageOverlayContext/MessageOverlayContext.tsx index b5d3d3aa07..fe78faf35f 100644 --- a/package/src/contexts/messageOverlayContext/MessageOverlayContext.tsx +++ b/package/src/contexts/messageOverlayContext/MessageOverlayContext.tsx @@ -2,7 +2,7 @@ import React, { PropsWithChildren, useContext } from 'react'; import type { ImageProps } from 'react-native'; -import type { Attachment, TranslationLanguages } from 'stream-chat'; +import { Attachment, TranslationLanguages } from 'stream-chat'; import { useResettableState } from './hooks/useResettableState'; @@ -46,6 +46,7 @@ export type MessageOverlayData< otherAttachments?: Attachment[]; OverlayReactionList?: React.ComponentType>; ownCapabilities?: OwnCapabilitiesContextValue; + Poll?: React.ComponentType; supportedReactions?: ReactionData[]; threadList?: boolean; userLanguage?: TranslationLanguages; diff --git a/package/src/contexts/messagesContext/MessagesContext.tsx b/package/src/contexts/messagesContext/MessagesContext.tsx index fe5b441da7..c69c373df5 100644 --- a/package/src/contexts/messagesContext/MessagesContext.tsx +++ b/package/src/contexts/messagesContext/MessagesContext.tsx @@ -4,6 +4,7 @@ import type { TouchableOpacityProps } from 'react-native'; import type { Attachment, ChannelState, MessageResponse } from 'stream-chat'; +import { PollContentProps } from '../../components'; import type { AttachmentProps } from '../../components/Attachment/Attachment'; import type { AttachmentActionsProps } from '../../components/Attachment/AttachmentActions'; import type { AudioAttachmentProps } from '../../components/Attachment/AudioAttachment'; @@ -60,7 +61,13 @@ import { DEFAULT_BASE_CONTEXT_VALUE } from '../utils/defaultBaseContextValue'; import { getDisplayName } from '../utils/getDisplayName'; import { isTestEnvironment } from '../utils/isTestEnvironment'; -export type MessageContentType = 'attachments' | 'files' | 'gallery' | 'quoted_reply' | 'text'; +export type MessageContentType = + | 'attachments' + | 'files' + | 'gallery' + | 'quoted_reply' + | 'poll' + | 'text'; export type DeletedMessagesVisibilityType = 'always' | 'never' | 'receiver' | 'sender'; export type MessagesContextValue< @@ -350,6 +357,8 @@ export type MessagesContextValue< handleRetry?: (message: MessageType) => Promise; /** Handler to access when a thread reply action is invoked */ handleThreadReply?: (message: MessageType) => Promise; + /** A flag specifying whether the poll creation button is available or not. */ + hasCreatePoll?: boolean; /** Handler to deal with custom memoization logic of Attachment */ isAttachmentEqual?: ( prevAttachment: Attachment, @@ -502,7 +511,11 @@ export type MessagesContextValue< * ``` */ onPressMessage?: (payload: MessageTouchableHandlerPayload) => void; - + /** + * Override the entire content of the Poll component. The component has full access to the + * usePollState() and usePollContext() hooks. + * */ + PollContent?: React.ComponentType; /** * Full override of the reaction function on Message and Message Overlay * diff --git a/package/src/contexts/overlayContext/OverlayContext.tsx b/package/src/contexts/overlayContext/OverlayContext.tsx index 525e9f5061..08db2e4f40 100644 --- a/package/src/contexts/overlayContext/OverlayContext.tsx +++ b/package/src/contexts/overlayContext/OverlayContext.tsx @@ -43,6 +43,7 @@ export type OverlayProviderProps< | 'attachmentSelectionBarHeight' | 'bottomInset' | 'CameraSelectorIcon' + | 'CreatePollIcon' | 'FileSelectorIcon' | 'ImageSelectorIcon' | 'topInset' diff --git a/package/src/contexts/overlayContext/OverlayProvider.tsx b/package/src/contexts/overlayContext/OverlayProvider.tsx index adfb264b5e..ca5bbfe881 100644 --- a/package/src/contexts/overlayContext/OverlayProvider.tsx +++ b/package/src/contexts/overlayContext/OverlayProvider.tsx @@ -27,6 +27,7 @@ import { ImageSelectorIcon as DefaultImageSelectorIcon } from '../../components/ import { ImageGallery } from '../../components/ImageGallery/ImageGallery'; import { MessageOverlay } from '../../components/MessageOverlay/MessageOverlay'; import { OverlayBackdrop } from '../../components/MessageOverlay/OverlayBackdrop'; +import { CreatePollIcon as DefaultCreatePollIcon } from '../../components/Poll/components/CreatePollIcon'; import { useStreami18n } from '../../hooks/useStreami18n'; import { useViewport } from '../../hooks/useViewport'; @@ -99,6 +100,7 @@ export const OverlayProvider = < }, 600); } }, + CreatePollIcon = DefaultCreatePollIcon, FileSelectorIcon = DefaultFileSelectorIcon, giphyVersion, i18nInstance, @@ -200,6 +202,7 @@ export const OverlayProvider = < bottomInset, CameraSelectorIcon, closePicker: () => closePicker(bottomSheetRef), + CreatePollIcon, FileSelectorIcon, ImageSelectorIcon, openPicker: () => openPicker(bottomSheetRef), diff --git a/package/src/contexts/ownCapabilitiesContext/OwnCapabilitiesContext.tsx b/package/src/contexts/ownCapabilitiesContext/OwnCapabilitiesContext.tsx index 6c3de984e8..75d9330059 100644 --- a/package/src/contexts/ownCapabilitiesContext/OwnCapabilitiesContext.tsx +++ b/package/src/contexts/ownCapabilitiesContext/OwnCapabilitiesContext.tsx @@ -6,14 +6,17 @@ import { isTestEnvironment } from '../utils/isTestEnvironment'; export const allOwnCapabilities = { banChannelMembers: 'ban-channel-members', + castPollVote: 'cast-poll-vote', deleteAnyMessage: 'delete-any-message', deleteOwnMessage: 'delete-own-message', flagMessage: 'flag-message', pinMessage: 'pin-message', + queryPollVotes: 'query-poll-votes', quoteMessage: 'quote-message', readEvents: 'read-events', sendLinks: 'send-links', sendMessage: 'send-message', + sendPoll: 'send-poll', sendReaction: 'send-reaction', sendReply: 'send-reply', sendTypingEvents: 'send-typing-events', diff --git a/package/src/contexts/pollContext/createPollContentContext.tsx b/package/src/contexts/pollContext/createPollContentContext.tsx new file mode 100644 index 0000000000..a206587ee5 --- /dev/null +++ b/package/src/contexts/pollContext/createPollContentContext.tsx @@ -0,0 +1,49 @@ +import React, { PropsWithChildren, useContext } from 'react'; + +import { CreatePollData } from 'stream-chat'; + +import { MessageInputContextValue } from '../messageInputContext/MessageInputContext'; +import { DEFAULT_BASE_CONTEXT_VALUE } from '../utils/defaultBaseContextValue'; + +import { isTestEnvironment } from '../utils/isTestEnvironment'; + +export type CreatePollContentContextValue = { + createAndSendPoll: (pollData: CreatePollData) => Promise; + sendMessage: MessageInputContextValue['sendMessage']; + /** + * A property that defines the constant height of the options within the poll creation screen. + * + * **Default: ** 71 + */ + closePollCreationDialog?: () => void; + createPollOptionHeight?: number; +}; + +export const CreatePollContentContext = React.createContext( + DEFAULT_BASE_CONTEXT_VALUE as CreatePollContentContextValue, +); + +export const CreatePollContentProvider = ({ + children, + value, +}: PropsWithChildren<{ + value: CreatePollContentContextValue; +}>) => ( + + {children} + +); + +export const useCreatePollContentContext = () => { + const contextValue = useContext( + CreatePollContentContext, + ) as unknown as CreatePollContentContextValue; + + if (contextValue === DEFAULT_BASE_CONTEXT_VALUE && !isTestEnvironment()) { + throw new Error( + `The useCreatePollContentContext hook was called outside of the CreatePollContentContext provider. Make sure you have configured the CreatePollContent component correctly - https://getstream.io/chat/docs/sdk/reactnative/basics/hello_stream_chat/#channel`, + ); + } + + return contextValue; +}; diff --git a/package/src/contexts/pollContext/index.ts b/package/src/contexts/pollContext/index.ts new file mode 100644 index 0000000000..85da74240e --- /dev/null +++ b/package/src/contexts/pollContext/index.ts @@ -0,0 +1,2 @@ +export * from './createPollContentContext'; +export * from './pollContext'; diff --git a/package/src/contexts/pollContext/pollContext.tsx b/package/src/contexts/pollContext/pollContext.tsx new file mode 100644 index 0000000000..d4a5e247cd --- /dev/null +++ b/package/src/contexts/pollContext/pollContext.tsx @@ -0,0 +1,43 @@ +import React, { PropsWithChildren, useContext } from 'react'; + +import { Poll } from 'stream-chat'; + +import { MessageType } from '../../components'; +import type { DefaultStreamChatGenerics } from '../../types/types'; +import { DEFAULT_BASE_CONTEXT_VALUE } from '../utils/defaultBaseContextValue'; + +import { isTestEnvironment } from '../utils/isTestEnvironment'; + +export type PollContextValue< + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +> = { + message: MessageType; + poll: Poll; +}; + +export const PollContext = React.createContext(DEFAULT_BASE_CONTEXT_VALUE as PollContextValue); + +export const PollContextProvider = < + StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics, +>({ + children, + value, +}: PropsWithChildren<{ + value: PollContextValue; +}>) => ( + + {children} + +); + +export const usePollContext = () => { + const contextValue = useContext(PollContext) as unknown as PollContextValue; + + if (contextValue === DEFAULT_BASE_CONTEXT_VALUE && !isTestEnvironment()) { + throw new Error( + `The useCreatePollContext hook was called outside of the PollContext provider. Make sure you have configured the Poll component correctly - https://getstream.io/chat/docs/sdk/reactnative/basics/hello_stream_chat/#channel`, + ); + } + + return contextValue; +}; diff --git a/package/src/contexts/themeContext/utils/theme.ts b/package/src/contexts/themeContext/utils/theme.ts index 4d3ab825df..acb6579e92 100644 --- a/package/src/contexts/themeContext/utils/theme.ts +++ b/package/src/contexts/themeContext/utils/theme.ts @@ -7,13 +7,18 @@ export const DEFAULT_STATUS_ICON_SIZE = 16; export const Colors = { accent_blue: '#005FFF', + accent_dark_blue: '#005DFF', + accent_error: '#FF3842', accent_green: '#20E070', + accent_info: '#1FE06F', accent_red: '#FF3742', bg_gradient_end: '#F7F7F7', bg_gradient_start: '#FCFCFC', + bg_user: '#F7F7F8', black: '#000000', blue_alice: '#E9F2FF', border: '#00000014', // 14 = 8% opacity; top: x=0, y=-1; bottom: x=0, y=1 + disabled: '#B4BBBA', grey: '#7A7A7A', grey_dark: '#72767E', grey_gainsboro: '#DBDBDB', @@ -613,6 +618,120 @@ export type Theme = { unSelectedIcon: IconProps; }; }; + poll: { + allOptions: { + listContainer: ViewStyle; + titleContainer: ViewStyle; + titleText: TextStyle; + wrapper: ViewStyle; + }; + answersList: { + buttonContainer: ViewStyle; + container: ViewStyle; + item: { + answerText: TextStyle; + container: ViewStyle; + infoContainer: ViewStyle; + userInfoContainer: ViewStyle; + }; + }; + button: { container: ViewStyle; text: TextStyle }; + createContent: { + addComment: { + title: TextStyle; + wrapper: ViewStyle; + }; + anonymousPoll: { + title: TextStyle; + wrapper: ViewStyle; + }; + headerContainer: ViewStyle; + maxVotes: { + input: TextStyle; + validationText: TextStyle; + wrapper: ViewStyle; + }; + multipleAnswers: { + row: ViewStyle; + title: TextStyle; + wrapper: ViewStyle; + }; + name: { + input: TextStyle; + title: TextStyle; + }; + pollOptions: { + addOption: { + text: TextStyle; + wrapper: ViewStyle; + }; + container: ViewStyle; + optionStyle: { + input: TextStyle; + validationErrorText: TextStyle; + wrapper: ViewStyle; + }; + title: TextStyle; + }; + scrollView: ViewStyle; + sendButton: ViewStyle; + suggestOption: { + title: TextStyle; + wrapper: ViewStyle; + }; + }; + fullResults: { + container: ViewStyle; + contentContainer: ViewStyle; + headerContainer: ViewStyle; + headerText: TextStyle; + }; + inputDialog: { + button: TextStyle; + buttonContainer: ViewStyle; + container: ViewStyle; + input: TextStyle; + title: TextStyle; + transparentContainer: ViewStyle; + }; + message: { + container: ViewStyle; + header: { + subtitle: TextStyle; + title: TextStyle; + }; + option: { + container: ViewStyle; + progressBar: ViewStyle; + progressBarEmptyFill: string; + progressBarVotedFill: string; + progressBarWinnerFill: string; + text: TextStyle; + voteButtonActive: string; + voteButtonContainer: ViewStyle; + voteButtonInactive: string; + votesContainer: ViewStyle; + wrapper: ViewStyle; + }; + optionsWrapper: ViewStyle; + }; + modalHeader: { + container: ViewStyle; + title: TextStyle; + }; + results: { + container: ViewStyle; + item: { + container: ViewStyle; + headerContainer: ViewStyle; + title: TextStyle; + voteCount: TextStyle; + }; + scrollView: ViewStyle; + title: TextStyle; + vote: { container: ViewStyle; dateText: TextStyle; userName: TextStyle }; + }; + }; progressControl: { container: ViewStyle; filledColor: ColorValue; @@ -1242,6 +1361,120 @@ export const defaultTheme: Theme = { unSelectedIcon: {}, }, }, + poll: { + allOptions: { + listContainer: {}, + titleContainer: {}, + titleText: {}, + wrapper: {}, + }, + answersList: { + buttonContainer: {}, + container: {}, + item: { + answerText: {}, + container: {}, + infoContainer: {}, + userInfoContainer: {}, + }, + }, + button: { container: {}, text: {} }, + createContent: { + addComment: { + title: {}, + wrapper: {}, + }, + anonymousPoll: { + title: {}, + wrapper: {}, + }, + headerContainer: {}, + maxVotes: { + input: {}, + validationText: {}, + wrapper: {}, + }, + multipleAnswers: { + row: {}, + title: {}, + wrapper: {}, + }, + name: { + input: {}, + title: {}, + }, + pollOptions: { + addOption: { + text: {}, + wrapper: {}, + }, + container: {}, + optionStyle: { + input: {}, + validationErrorText: {}, + wrapper: {}, + }, + title: {}, + }, + scrollView: {}, + sendButton: {}, + suggestOption: { + title: {}, + wrapper: {}, + }, + }, + fullResults: { + container: {}, + contentContainer: {}, + headerContainer: {}, + headerText: {}, + }, + inputDialog: { + button: {}, + buttonContainer: {}, + container: {}, + input: {}, + title: {}, + transparentContainer: {}, + }, + message: { + container: {}, + header: { + subtitle: {}, + title: {}, + }, + option: { + container: {}, + progressBar: {}, + progressBarEmptyFill: '', + progressBarVotedFill: '', + progressBarWinnerFill: '', + text: {}, + voteButtonActive: '', + voteButtonContainer: {}, + voteButtonInactive: '', + votesContainer: {}, + wrapper: {}, + }, + optionsWrapper: {}, + }, + modalHeader: { + container: {}, + title: {}, + }, + results: { + container: {}, + item: { + container: {}, + headerContainer: {}, + title: {}, + voteCount: {}, + }, + scrollView: {}, + title: {}, + vote: { container: {}, dateText: {}, userName: {} }, + }, + }, progressControl: { container: {}, filledColor: '', diff --git a/package/src/i18n/en.json b/package/src/i18n/en.json index 8990b9337f..eb3c96f8ad 100644 --- a/package/src/i18n/en.json +++ b/package/src/i18n/en.json @@ -1,17 +1,23 @@ { "1 Reply": "1 Reply", "1 Thread Reply": "1 Thread Reply", + "Add a comment": "Add a comment", + "Add an option": "Add an option", "Allow access to your Gallery": "Allow access to your Gallery", "Allow camera access in device settings": "Allow camera access in device settings", "Also send to channel": "Also send to channel", + "Anonymous": "Anonymous", + "Anonymous poll": "Anonymous poll", "Are you sure you want to permanently delete this message?": "Are you sure you want to permanently delete this message?", "Are you sure?": "Are you sure?", + "Ask a question": "Ask a question", "Ban User": "Ban User", "Block User": "Block User", "Cancel": "Cancel", "Cannot Flag Message": "Cannot Flag Message", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "Consider how your comment might make others feel and be sure to follow our Community Guidelines", "Copy Message": "Copy Message", + "Create Poll": "Create Poll", "Delete": "Delete", "Delete Message": "Delete Message", "Device camera is used to take photos or videos.": "Device camera is used to take photos or videos.", @@ -22,6 +28,7 @@ "Editing Message": "Editing Message", "Emoji matching": "Emoji matching", "Empty message...": "Empty message...", + "End Vote": "End Vote", "Error loading": "Error loading", "Error loading channel list...": "Error loading channel list...", "Error loading messages for this channel...": "Error loading messages for this channel...", @@ -41,9 +48,11 @@ "Loading threads...": "Loading threads...", "Loading...": "Loading...", "Maximum number of files reached": "Maximum number of files reached", + "Maximum votes per person": "Maximum votes per person", "Message Reactions": "Message Reactions", "Message deleted": "Message deleted", "Message flagged": "Message flagged", + "Multiple answers": "Multiple answers", "Mute User": "Mute User", "No chats here yet…": "No chats here yet…", "No threads here yet": "No threads here yet", @@ -52,6 +61,8 @@ "Ok": "Ok", "Only visible to you": "Only visible to you", "Open Settings": "Open Settings", + "Option": "Option", + "Options": "Options", "Photo": "Photo", "Photos and Videos": "Photos and Videos", "Pin to Conversation": "Pin to Conversation", @@ -59,28 +70,48 @@ "Please allow Audio permissions in settings.": "Please allow Audio permissions in settings.", "Please enable access to your photos and videos so you can share them.": "Please enable access to your photos and videos so you can share them.", "Please select a channel first": "Please select a channel first", + "Poll Comments": "Poll Comments", + "Poll Options": "Poll Options", + "Poll Results": "Poll Results", + "Questions": "Questions", "Reconnecting...": "Reconnecting...", "Reply": "Reply", "Reply to Message": "Reply to Message", "Resend": "Resend", + "SEND": "SEND", "Search GIFs": "Search GIFs", + "See all {{count}} options_one": "See all {{count}} options", + "See all {{count}} options_other": "See all {{count}} options", "Select More Photos": "Select More Photos", + "Select one": "Select one", + "Select one or more": "Select one or more", + "Select up to {{count}}_one": "Select up to {{count}}", + "Select up to {{count}}_other": "Select up to {{count}}", "Send Anyway": "Send Anyway", "Send a message": "Send a message", "Sending links is not allowed in this conversation": "Sending links is not allowed in this conversation", + "Show All": "Show All", "Slow mode ON": "Slow mode ON", + "Suggest an option": "Suggest an option", "The message has been reported to a moderator.": "The message has been reported to a moderator.", "The source message was deleted": "The source message was deleted", + "This is already an option": "This is already an option", "This reply was deleted": "This reply was deleted", "Thread Reply": "Thread Reply", + "Type a number from 2 to 10": "Type a number from 2 to 10", "Unban User": "Unban User", "Unblock User": "Unblock User", "Unknown User": "Unknown User", "Unmute User": "Unmute User", "Unpin from Conversation": "Unpin from Conversation", "Unread Messages": "Unread Messages", + "Update your comment": "Update your comment", "Video": "Video", + "View Results": "View Results", + "View {{count}} comments_one": "View {{count}} comment", + "View {{count}} comments_other": "View {{count}} comments", "Voice message": "Voice message", + "Vote ended": "Vote ended", "You": "You", "You can't send messages in this channel": "You can't send messages in this channel", "replied to": "replied to", @@ -90,6 +121,7 @@ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}", + "timestamp/PollVote": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Yesterday]\", \"lastWeek\":\"dddd\", \"nextDay\":\"[Tomorrow]\", \"nextWeek\":\"dddd [at] LT\", \"sameDay\":\"LT\", \"sameElse\":\"L\"}) }}", "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Yesterday]\", \"lastWeek\":\"dddd\", \"nextDay\":\"[Tomorrow]\", \"nextWeek\":\"dddd [at] LT\", \"sameDay\":\"LT\", \"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} and {{ nonSelfUserLength }} more are typing", @@ -97,5 +129,7 @@ "{{ replyCount }} Replies": "{{ replyCount }} Replies", "{{ replyCount }} Thread Replies": "{{ replyCount }} Thread Replies", "{{ user }} is typing": "{{ user }} is typing", + "{{count}} votes_one": "{{count}} vote", + "{{count}} votes_other": "{{count}} votes", "🏙 Attachment...": "🏙 Attachment..." } diff --git a/package/src/i18n/es.json b/package/src/i18n/es.json index 968ddbddb8..576a1778dc 100644 --- a/package/src/i18n/es.json +++ b/package/src/i18n/es.json @@ -1,17 +1,23 @@ { "1 Reply": "1 respuesta", "1 Thread Reply": "1 respuesta de hilo", + "Add a comment": "Agregar un comentario", + "Add an option": "Agregar una opción", "Allow access to your Gallery": "Permitir acceso a tu galería", "Allow camera access in device settings": "Permitir el acceso a la cámara en la configuración del dispositivo", "Also send to channel": "También enviar al canal", + "Anonymous": "Anónimo", + "Anonymous poll": "Encuesta anónima", "Are you sure you want to permanently delete this message?": "¿Estás seguro de que deseas eliminar permanentemente este mensaje?", "Are you sure?": "¿Estás seguro?", + "Ask a question": "Hacer una pregunta", "Ban User": "Bloquear Usuario", "Block User": "Bloquear usuario", "Cancel": "Cancelar", "Cannot Flag Message": "No se puede reportar el mensaje", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "Considera cómo tu comentario podría hacer sentir a los demás y asegúrate de seguir nuestras Normas de la Comunidad", "Copy Message": "Copiar mensaje", + "Create Poll": "Crear encuesta", "Delete": "Eliminar", "Delete Message": "Eliminar mensaje", "Device camera is used to take photos or videos.": "La cámara del dispositivo se utiliza para tomar fotografías o vídeos.", @@ -22,6 +28,7 @@ "Editing Message": "Editando mensaje", "Emoji matching": "Coincidencia de emoji", "Empty message...": "Mensaje vacío...", + "End Vote": "Finalizar votación", "Error loading": "Error al cargar", "Error loading channel list...": "Error al cargar la lista de canales...", "Error loading messages for this channel...": "Error al cargar los mensajes de este canal...", @@ -41,9 +48,11 @@ "Loading threads...": "Cargando hilos...", "Loading...": "Cargando...", "Maximum number of files reached": "Número máximo de archivos alcanzado", + "Maximum votes per person": "Máximo de votos por persona", "Message Reactions": "Reacciones al mensaje", "Message deleted": "Mensaje eliminado", "Message flagged": "Mensaje reportado", + "Multiple answers": "Respuestas múltiples", "Mute User": "Silenciar usuario", "No chats here yet…": "No hay chats aquí todavía...", "No threads here yet": "Aún no hay hilos aquí", @@ -52,6 +61,8 @@ "Ok": "Aceptar", "Only visible to you": "Solo visible para ti", "Open Settings": "Configuración abierta", + "Option": "Opción", + "Options": "Opciones", "Photo": "Foto", "Photos and Videos": "Fotos y videos", "Pin to Conversation": "Fijar a la conversación", @@ -59,28 +70,51 @@ "Please allow Audio permissions in settings.": "Por favor, permita los permisos de audio en la configuración.", "Please enable access to your photos and videos so you can share them.": "Por favor, habilita el acceso a tus fotos y videos para poder compartirlos.", "Please select a channel first": "Por favor, selecciona primero un canal", + "Poll Comments": "Comentarios de la encuesta", + "Poll Options": "Opciones de la encuesta", + "Poll Results": "Resultados de la encuesta", + "Questions": "Preguntas", "Reconnecting...": "Reconectando...", "Reply": "Responder", "Reply to Message": "Responder al mensaje", "Resend": "Reenviar", + "SEND": "ENVIAR", "Search GIFs": "Buscar GIFs", + "See all {{count}} options_many": "Ver las {{count}} opciones", + "See all {{count}} options_one": "Ver las {{count}} opciones", + "See all {{count}} options_other": "Ver las {{count}} opciones", "Select More Photos": "Seleccionar más fotos", + "Select one": "Seleccionar una", + "Select one or more": "Seleccionar una o más", + "Select up to {{count}}_many": "Selecciona hasta {{count}}", + "Select up to {{count}}_one": "Selecciona hasta {{count}}", + "Select up to {{count}}_other": "Selecciona hasta {{count}}", "Send Anyway": "Enviar de todos modos", "Send a message": "Enviar un mensaje", "Sending links is not allowed in this conversation": "No está permitido enviar enlaces en esta conversación", + "Show All": "Mostrar todo", "Slow mode ON": "Modo lento ACTIVADO", + "Suggest an option": "Sugerir una opción", "The message has been reported to a moderator.": "El mensaje ha sido reportado a un moderador.", "The source message was deleted": "El mensaje original fue eliminado", + "This is already an option": "Esto ya es una opción", "This reply was deleted": "Esta respuesta fue eliminada", "Thread Reply": "Respuesta de hilo", + "Type a number from 2 to 10": "Escribe un número de 2 a 10", "Unban User": "Desbloquear usuario", "Unblock User": "Usuario desconocido", "Unknown User": "Desbloquear Usuario", "Unmute User": "Activar sonido del usuario", "Unpin from Conversation": "Desmarcar de la conversación", "Unread Messages": "Mensajes no leídos", + "Update your comment": "Actualizar tu comentario", "Video": "Video", + "View Results": "Ver resultados", + "View {{count}} comments_many": "Ver {{count}} comentarios", + "View {{count}} comments_one": "Ver {{count}} comentario", + "View {{count}} comments_other": "Ver {{count}} comentarios", "Voice message": "Mensaje de voz", + "Vote ended": "Votación finalizada", "You": "Tú", "You can't send messages in this channel": "No puedes enviar mensajes en este canal", "replied to": "respondió a", @@ -90,6 +124,7 @@ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}", + "timestamp/PollVote": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Ayer]\", \"lastWeek\":\"dddd\", \"nextDay\":\"[Mañana]\", \"nextWeek\":\"dddd [a las] LT\", \"sameDay\":\"LT\", \"sameElse\":\"L\"}) }}", "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Ayer]\", \"lastWeek\":\"dddd\", \"nextDay\":\"[Mañana]\", \"nextWeek\":\"dddd [a las] LT\", \"sameDay\":\"LT\", \"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} y {{ nonSelfUserLength }} más están escribiendo", @@ -97,5 +132,8 @@ "{{ replyCount }} Replies": "{{ replyCount }} Respuestas", "{{ replyCount }} Thread Replies": "{{ replyCount }} respuestas de hilo", "{{ user }} is typing": "{{ user }} está escribiendo", + "{{count}} votes_many": "{{count}} votos", + "{{count}} votes_one": "{{count}} voto", + "{{count}} votes_other": "{{count}} votos", "🏙 Attachment...": "🏙 Adjunto..." } diff --git a/package/src/i18n/fr.json b/package/src/i18n/fr.json index 01d8b3f46a..06afa66719 100644 --- a/package/src/i18n/fr.json +++ b/package/src/i18n/fr.json @@ -1,17 +1,23 @@ { "1 Reply": "1 Réponse", "1 Thread Reply": "Réponse à 1 fil", + "Add a comment": "Ajouter un commentaire", + "Add an option": "Ajouter une option", "Allow access to your Gallery": "Autoriser l'accès à votre galerie", "Allow camera access in device settings": "Autoriser l'accès à la caméra dans les paramètres de l'appareil", "Also send to channel": "Envoyer également à la chaîne", + "Anonymous": "Anonyme", + "Anonymous poll": "Sondage anonyme", "Are you sure you want to permanently delete this message?": "Êtes-vous sûr de vouloir supprimer définitivement ce message?", "Are you sure?": "Es-tu sûr ?", + "Ask a question": "Poser une question", "Ban User": "Bannir Utilisateur", "Block User": "Bloquer un utilisateur", "Cancel": "Annuler", "Cannot Flag Message": "Impossible de signaler le message", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "Considérez comment votre commentaire pourrait faire sentir les autres et assurez-vous de suivre nos directives communautaires", "Copy Message": "Copier le message", + "Create Poll": "Créer un sondage", "Delete": "Supprimer", "Delete Message": "Supprimer un message", "Device camera is used to take photos or videos.": "L'appareil photo de l'appareil est utilisé pour prendre des photos ou des vidéos.", @@ -22,6 +28,7 @@ "Editing Message": "Édite un message", "Emoji matching": "Correspondance Emoji", "Empty message...": "Message vide...", + "End Vote": "Fin du vote", "Error loading": "Erreur lors du chargement", "Error loading channel list...": "Erreur lors du chargement de la liste de canaux...", "Error loading messages for this channel...": "Erreur lors du chargement des messages de ce canal...", @@ -41,9 +48,11 @@ "Loading threads...": "Chargement des fils...", "Loading...": "Chargement...", "Maximum number of files reached": "Nombre maximal de fichiers atteint", + "Maximum votes per person": "Maximum de votes par personne", "Message Reactions": "Réactions aux messages", "Message deleted": "Message supprimé", "Message flagged": "Message signalé", + "Multiple answers": "Réponses multiples", "Mute User": "Utilisateur muet", "No chats here yet…": "Pas de discussions ici pour le moment…", "No threads here yet": "Aucun fil ici pour le moment", @@ -52,6 +61,8 @@ "Ok": "Ok", "Only visible to you": "Seulement visible par vous", "Open Settings": "Ouvrir les paramètres", + "Option": "Option", + "Options": "Options", "Photo": "Photo", "Photos and Videos": "Photos et vidéos", "Pin to Conversation": "Épingler à la conversation", @@ -59,28 +70,51 @@ "Please allow Audio permissions in settings.": "Veuillez autoriser les permissions audio dans les paramètres.", "Please enable access to your photos and videos so you can share them.": "Veuillez autoriser l'accès à vos photos et vidéos afin de pouvoir les partager.", "Please select a channel first": "Veuillez d'abord selectionnez un canal", + "Poll Comments": "Commentaires du sondage", + "Poll Options": "Options du sondage", + "Poll Results": "Résultats du sondage", + "Questions": "Questions", "Reconnecting...": "Se Reconnecter...", "Reply": "Répondre", "Reply to Message": "Répondre au message", "Resend": "Renvoyer", + "SEND": "ENVOYER", "Search GIFs": "Rechercher des GIF", + "See all {{count}} options_many": "Voir les {{count}} options", + "See all {{count}} options_one": "Voir les {{count}} options", + "See all {{count}} options_other": "Voir les {{count}} options", "Select More Photos": "Sélectionner plus de photos", + "Select one": "Sélectionner une", + "Select one or more": "Sélectionner une ou plusieurs", + "Select up to {{count}}_many": "Sélectionnez jusqu'à {{count}}", + "Select up to {{count}}_one": "Sélectionnez jusqu'à {{count}}", + "Select up to {{count}}_other": "Sélectionnez jusqu'à {{count}}", "Send Anyway": "Envoyer quand même", "Send a message": "Envoyer un message", "Sending links is not allowed in this conversation": "Sending links is not allowed in this conversation", + "Show All": "Afficher tout", "Slow mode ON": "Mode lent activé", + "Suggest an option": "Suggérer une option", "The message has been reported to a moderator.": "Le message a été signalé à un modérateur.", "The source message was deleted": "Le message source a été supprimé", + "This is already an option": "C'est déjà une option", "This reply was deleted": "Cette réponse a été supprimée", "Thread Reply": "Réponse à la discussion", + "Type a number from 2 to 10": "Entrez un nombre de 2 à 10", "Unban User": "Débannir Utilisateur", "Unblock User": "Débloquer Utilisateur", "Unknown User": "Utilisateur inconnu", "Unmute User": "Activer le son de Utilisateur", "Unpin from Conversation": "Décrocher de la conversation", "Unread Messages": "Messages non lus", + "Update your comment": "Mettre à jour votre commentaire", "Video": "Vidéo", + "View Results": "Voir les résultats", + "View {{count}} comments_many": "Voir {{count}} commentaires", + "View {{count}} comments_one": "Voir {{count}} commentaire", + "View {{count}} comments_other": "Voir {{count}} commentaires", "Voice message": "Message vocal", + "Vote ended": "Vote terminé", "You": "Toi", "You can't send messages in this channel": "You can't send messages in this channel", "replied to": "a répondu à", @@ -90,6 +124,7 @@ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}", + "timestamp/PollVote": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Hier]\", \"lastWeek\":\"dddd\", \"nextDay\":\"[Demain]\", \"nextWeek\":\"dddd [à] LT\", \"sameDay\":\"LT\", \"sameElse\":\"L\"}) }}", "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Hier]\", \"lastWeek\":\"dddd\", \"nextDay\":\"[Demain]\", \"nextWeek\":\"dddd [à] LT\", \"sameDay\":\"LT\", \"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} et {{ nonSelfUserLength }} autres sont en train d'écrire", @@ -97,5 +132,8 @@ "{{ replyCount }} Replies": "{{ replyCount }} Réponses", "{{ replyCount }} Thread Replies": "{{replyCount}} Réponses à la discussion", "{{ user }} is typing": "{{ user }} est en train d'écrire", + "{{count}} votes_many": "{{count}} votes", + "{{count}} votes_one": "{{count}} vote", + "{{count}} votes_other": "{{count}} votes", "🏙 Attachment...": "🏙 Pièce jointe..." } diff --git a/package/src/i18n/he.json b/package/src/i18n/he.json index 9322ff280b..48f3b0df02 100644 --- a/package/src/i18n/he.json +++ b/package/src/i18n/he.json @@ -1,17 +1,23 @@ { "1 Reply": "תגובה אחת", "1 Thread Reply": "תגובה אחת לשרשור", + "Add a comment": "הוסף תגובה", + "Add an option": "הוסף אפשרות", "Allow access to your Gallery": "אפשר גישה לגלריה שלך", "Allow camera access in device settings": "אפשר גישה למצלמה בהגדרות המכשיר", "Also send to channel": "שלח/י הודעה לשיחה", + "Anonymous": "אנונימי", + "Anonymous poll": "סקר אנונימי", "Are you sure you want to permanently delete this message?": "האם את/ה בטוח/ה שאת/ה רוצה למחוק את ההודעה הזו לצמיתות?", "Are you sure?": "האם אתה בטוח?", + "Ask a question": "שאל שאלה", "Ban User": "לחסום משתמש", "Block User": "חסום משתמש", "Cancel": "ביטול", "Cannot Flag Message": "סימון הודעה לא אפשרי", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "שקול איך התגובה שלך עשויה להשפיע על אחרים ווודא שאתה עוקב אחר ההנחיות של הקהילה שלנו", "Copy Message": "העתק/י הודעה", + "Create Poll": "צור סקר", "Delete": "מחק", "Delete Message": "מחק/י הודעה", "Device camera is used to take photos or videos.": "מצלמת המכשיר משמשת לצילום תמונות או סרטונים.", @@ -22,6 +28,7 @@ "Editing Message": "הודעה בעריכה", "Emoji matching": "התאמת אמוג'י", "Empty message...": "הודעה ריקה...", + "End Vote": "סיים הצבעה", "Error loading": "שגיאה ארעה בעת הטעינה", "Error loading channel list...": "שגיאה ארעה בטעינת השיחות...", "Error loading messages for this channel...": "שגיאה ארעה בטעינת הודעות עבור שיחה זאת...", @@ -41,9 +48,11 @@ "Loading threads...": "טוען שרשורים...", "Loading...": "טוען...", "Maximum number of files reached": "הגעת למספר המרבי של קבצים", + "Maximum votes per person": "מקסימום הצבעות לאדם", "Message Reactions": "תגובות להודעה", "Message deleted": "ההודעה נמחקה", "Message flagged": "ההודעה סומנה", + "Multiple answers": "תשובות מרובות", "Mute User": "השתק/י משתמש", "No chats here yet…": "אין צ'אטים כאן עדיין...", "No threads here yet": "אין שרשורים כאן עדיין", @@ -52,6 +61,8 @@ "Ok": "אוקיי", "Only visible to you": "גלוי רק לך", "Open Settings": "פתח את ההגדרות", + "Option": "אפשרות", + "Options": "אפשרויות", "Photo": "תמונה", "Photos and Videos": "תמונות ווידאו", "Pin to Conversation": "הצמד/י לשיחה", @@ -59,28 +70,51 @@ "Please allow Audio permissions in settings.": "בבקשה, הרשה הרשאות שמע בהגדרות.", "Please enable access to your photos and videos so you can share them.": "אפשר/י גישה לתמונות ולסרטונים שלך כדי שתוכל/י לשתף אותם.", "Please select a channel first": "אנא בחר/י שיחה תחילה", + "Poll Comments": "תגובות לסקר", + "Poll Options": "אפשרויות הסקר", + "Poll Results": "תוצאות הסקר", + "Questions": "שאלות", "Reconnecting...": "מתחבר מחדש...", "Reply": "השב/י", "Reply to Message": "השב/י להודעה", "Resend": "שלח/י שוב", + "SEND": "שלח", "Search GIFs": "חפש/י GIFs", + "See all {{count}} options_one": "הצג את כל {{count}} האפשרויות", + "See all {{count}} options_other": "הצג את כל {{count}} האפשרויות", + "See all {{count}} options_two": "הצג את כל {{count}} האפשרויות", "Select More Photos": "בחר עוד תמונות", + "Select one": "בחר אחת", + "Select one or more": "בחר אחת או יותר", + "Select up to {{count}}_one": "בחר עד {{count}}", + "Select up to {{count}}_other": "בחר עד {{count}}", + "Select up to {{count}}_two": "בחר עד {{count}}", "Send Anyway": "שלח בכל זאת", "Send a message": "שלח/י הודעה", "Sending links is not allowed in this conversation": "שליחת קישורים אינה מותרת בשיחה זו", + "Show All": "הצג הכל", "Slow mode ON": "מצב איטי מופעל", + "Suggest an option": "הצע אפשרות", "The message has been reported to a moderator.": "ההודעה דווחה למנהל", "The source message was deleted": "ההודעה המקורית נמחקה", + "This is already an option": "זו כבר אפשרות קיימת", "This reply was deleted": "התגובה הזו נמחקה", "Thread Reply": "הגב/י בשרשור", + "Type a number from 2 to 10": "הקלד מספר בין 2 ל-10", "Unban User": "לבטל חסימת משתמש", "Unblock User": "בטל/י חסימת משתמש", "Unknown User": "משתמש לא ידוע", "Unmute User": "בטל/י השתקת משתמש", "Unpin from Conversation": "בטל/י הצמדה לשיחה", "Unread Messages": "הודעות שטרם נקרו", + "Update your comment": "עדכן את התגובה שלך", "Video": "וִידֵאוֹ", + "View Results": "הצג תוצאות", + "View {{count}} comments_one": "הצג {{count}} תגובה", + "View {{count}} comments_other": "הצג {{count}} תגובות", + "View {{count}} comments_two": "הצג {{count}} תגובות", "Voice message": "הודעת קול", + "Vote ended": "ההצבעה הסתיימה", "You": "את/ה", "You can't send messages in this channel": "את/ב לא יכול/ה לשלוח הודעות בשיחה זו", "replied to": "הגיב ל", @@ -90,6 +124,7 @@ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}", + "timestamp/PollVote": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[אתמול]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[מחר]\",\"nextWeek\":\"dddd [בשעה] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[אתמול]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[מחר]\",\"nextWeek\":\"dddd [בשעה] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} ו-{{ nonSelfUserLength }} משתמש/ים אחר/ים מקלידים", @@ -97,5 +132,8 @@ "{{ replyCount }} Replies": "{{ replyCount }} תגובות", "{{ replyCount }} Thread Replies": "{{ replyCount }} תגובות שרשור", "{{ user }} is typing": "{{ user }} מקליד/ה", + "{{count}} votes_one": "{{count}} הצבעה", + "{{count}} votes_other": "{{count}} הצבעות", + "{{count}} votes_two": "{{count}} הצבעות", "🏙 Attachment...": "🏙 קובץ מצורף..." } diff --git a/package/src/i18n/hi.json b/package/src/i18n/hi.json index 353c0333f3..a65b30cf97 100644 --- a/package/src/i18n/hi.json +++ b/package/src/i18n/hi.json @@ -1,17 +1,23 @@ { "1 Reply": "1 रिप्लाई", "1 Thread Reply": "1 धागा उत्तर", + "Add a comment": "एक टिप्पणी जोड़ें", + "Add an option": "एक विकल्प जोड़ें", "Allow access to your Gallery": "अपनी गैलरी तक पहुँचने की अनुमति दें", "Allow camera access in device settings": "डिवाइस सेटिंग्स में कैमरा एक्सेस की अनुमति दें", "Also send to channel": "चैनल को भी भेजें", + "Anonymous": "गुमनाम", + "Anonymous poll": "अनाम सर्वेक्षण", "Are you sure you want to permanently delete this message?": "क्या आप वाकई इस संदेश को स्थायी रूप से हटाना चाहते हैं?", "Are you sure?": "क्या आप सुनिश्चित हैं?", + "Ask a question": "एक प्रश्न पूछें", "Ban User": "उपयोगकर्ता को प्रतिबंधित करें", "Block User": "उपयोगकर्ता को रोक देना, ब्लॉक यूजर", "Cancel": "रद्द करें", "Cannot Flag Message": "मैसेज फ्लैग नहीं किया जा सकता है", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "ध्यान दें कि आपका संदेश दूसरों को कैसा लगा सकता है और सुनिश्चित हों कि आप हमारी सामुदायिक अनुशासन का पालन कर रहे हैं", "Copy Message": "संदेश की प्रतिलिपि बनाएँ", + "Create Poll": "सर्वेक्षण बनाएं", "Delete": "हटाएं", "Delete Message": "मैसेज को डिलीट करे", "Device camera is used to take photos or videos.": "डिवाइस कैमरे का उपयोग फ़ोटो या वीडियो लेने के लिए किया जाता है।", @@ -22,6 +28,7 @@ "Editing Message": "मैसेज बदला जा रहा है", "Emoji matching": "इमोजी मिलान", "Empty message...": "खाली संदेश...", + "End Vote": "वोट समाप्त करें", "Error loading": "लोड होने मे त्रुटि", "Error loading channel list...": "चैनल सूची लोड करने में त्रुटि...", "Error loading messages for this channel...": "इस चैनल के लिए मेसेजेस लोड करने में त्रुटि हुई...", @@ -41,9 +48,11 @@ "Loading threads...": "थ्रेड्स लोड हो रहे हैं...", "Loading...": "लोड हो रहा है...", "Maximum number of files reached": "फ़ाइलों की अधिकतम संख्या पहुँच गई", + "Maximum votes per person": "प्रति व्यक्ति अधिकतम वोट", "Message Reactions": "संदेश प्रतिक्रियाएँ", "Message deleted": "मैसेज हटा दिया गया", "Message flagged": "संदेश को ध्वजांकित किया गया", + "Multiple answers": "एक से अधिक उत्तर", "Mute User": "उपयोगकर्ता को म्यूट करें", "No chats here yet…": "अभी तक यहाँ कोई चैट नहीं है...", "No threads here yet": "यहाँ अभी तक कोई थ्रेड्स नहीं हैं", @@ -52,6 +61,8 @@ "Ok": "ठीक", "Only visible to you": "केवल आपको दिखाई दे रहा है", "Open Settings": "सेटिंग्स खोलें", + "Option": "विकल्प", + "Options": "विकल्प", "Photo": "तस्वीर", "Photos and Videos": "तस्वीरें और वीडियों", "Pin to Conversation": "बातचीत में पिन करें", @@ -59,28 +70,48 @@ "Please allow Audio permissions in settings.": "कृपया सेटिंग्स में ऑडियो की अनुमति दें।", "Please enable access to your photos and videos so you can share them.": "कृपया अपनी फ़ोटो और वीडियो तक पहुंच सक्षम करें ताकि आप उन्हें साझा कर सकें।", "Please select a channel first": "कृपया पहले एक चैनल चुनें", + "Poll Comments": "सर्वेक्षण टिप्पणियाँ", + "Poll Options": "सर्वेक्षण विकल्प", + "Poll Results": "सर्वेक्षण परिणाम", + "Questions": "प्रश्न", "Reconnecting...": "पुनः कनेक्ट हो...", "Reply": "मैसेज को रिप्लाई करे", "Reply to Message": "संदेश का जवाब दें", "Resend": "पुन: भेजें", + "SEND": "भेजें", "Search GIFs": "GIF खोजें", + "See all {{count}} options_one": "सभी {{count}} विकल्प देखें", + "See all {{count}} options_other": "सभी {{count}} विकल्प देखें", "Select More Photos": "अधिक फ़ोटो चुनें", + "Select one": "एक चुनें", + "Select one or more": "एक या अधिक चुनें", + "Select up to {{count}}_one": "{{count}} तक चुनें", + "Select up to {{count}}_other": "{{count}} तक चुनें", "Send Anyway": "फिर भी भेजें", "Send a message": "एक संदेश भेजें", "Sending links is not allowed in this conversation": "इस बातचीत में लिंक भेजने की अनुमति नहीं है", + "Show All": "सभी दिखाएं", "Slow mode ON": "स्लो मोड चालू", + "Suggest an option": "एक विकल्प सुझाएं", "The message has been reported to a moderator.": "संदेश एक मॉडरेटर को सूचित किया गया है।", "The source message was deleted": "स्रोत संदेश हटा दिया गया है", + "This is already an option": "यह पहले से एक विकल्प है", "This reply was deleted": "यह उत्तर हटा दिया गया है", "Thread Reply": "धागा जवाब", + "Type a number from 2 to 10": "2 से 10 के बीच एक संख्या दर्ज करें", "Unban User": "उपयोगकर्ता को अनब्लॉक करें", "Unblock User": "उपयोगकर्ता को अनब्लॉक करें", "Unknown User": "अज्ञात उपयोगकर्ता", "Unmute User": "उपयोगकर्ता को अनम्यूट करें", "Unpin from Conversation": "बातचीत से अनपिन करें", "Unread Messages": "अपठित संदेश", + "Update your comment": "अपनी टिप्पणी अपडेट करें", "Video": "वीडियो", + "View Results": "परिणाम देखें", + "View {{count}} comments_one": "{{count}} टिप्पणी देखें", + "View {{count}} comments_other": "{{count}} टिप्पणियाँ देखें", "Voice message": "वॉइस संदेश", + "Vote ended": "वोट समाप्त", "You": "आप", "You can't send messages in this channel": "आप इस चैनल में संदेश नहीं भेज सकते", "replied to": "को उत्तर दिया", @@ -90,6 +121,7 @@ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}", + "timestamp/PollVote": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[कल]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[कल]\",\"nextWeek\":\"dddd [को] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[कल]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[कल]\",\"nextWeek\":\"dddd [को] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} और {{ nonSelfUserLength }} अधिक टाइप कर रहे हैं", @@ -97,5 +129,7 @@ "{{ replyCount }} Replies": "{{ replyCount }} रिप्लाई", "{{ replyCount }} Thread Replies": "{{ replyCount }}} थ्रेड उत्तर", "{{ user }} is typing": "{{ user }} टाइप कर रहा है", + "{{count}} votes_one": "{{count}} वोट", + "{{count}} votes_other": "{{count}} वोट", "🏙 Attachment...": "🏙 अटैचमेंट..." } diff --git a/package/src/i18n/it.json b/package/src/i18n/it.json index e5c6c7cf0d..46799dd420 100644 --- a/package/src/i18n/it.json +++ b/package/src/i18n/it.json @@ -1,17 +1,23 @@ { "1 Reply": "1 Risposta", "1 Thread Reply": "1 Risposta alla Discussione", + "Add a comment": "Aggiungi un commento", + "Add an option": "Aggiungi un'opzione", "Allow access to your Gallery": "Consenti l'accesso alla tua galleria", "Allow camera access in device settings": "Consenti l'accesso alla fotocamera nelle impostazioni del dispositivo", "Also send to channel": "Invia anche al canale", + "Anonymous": "Anonimo", + "Anonymous poll": "Sondaggio anonimo", "Are you sure you want to permanently delete this message?": "Sei sicuro di voler eliminare definitivamente questo messaggio?", "Are you sure?": "Sei sicuro?", + "Ask a question": "Fai una domanda", "Ban User": "Blocca Utente", "Block User": "Blocca Utente", "Cancel": "Annulla", "Cannot Flag Message": "Impossibile Segnalare Messaggio", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "Considera come il tuo commento potrebbe far sentire gli altri e assicurati di seguire le nostre Linee guida della community", "Copy Message": "Copia Messaggio", + "Create Poll": "Crea sondaggio", "Delete": "Elimina", "Delete Message": "Cancella il Messaggio", "Device camera is used to take photos or videos.": "La fotocamera del dispositivo viene utilizzata per scattare foto o video.", @@ -22,6 +28,7 @@ "Editing Message": "Modificando il Messaggio", "Emoji matching": "Abbinamento emoji", "Empty message...": "Message vuoto...", + "End Vote": "Termina votazione", "Error loading": "Errore di caricamento", "Error loading channel list...": "Errore durante il caricamento della lista dei canali...", "Error loading messages for this channel...": "Errore durante il caricamento dei messaggi per questo canale...", @@ -41,9 +48,11 @@ "Loading threads...": "Caricamento dei thread...", "Loading...": "Caricamento...", "Maximum number of files reached": "Numero massimo di file raggiunto", + "Maximum votes per person": "Massimo voti per persona", "Message Reactions": "Reazioni ai Messaggi", "Message deleted": "Messaggio cancellato", "Message flagged": "Messaggio contrassegnato", + "Multiple answers": "Risposte multiple", "Mute User": "Utente Muto", "No chats here yet…": "Non ci sono ancora chat qui...", "No threads here yet": "Nessun thread qui ancora", @@ -52,6 +61,8 @@ "Ok": "Ok", "Only visible to you": "Visibile solo a te", "Open Settings": "Apri Impostazioni", + "Option": "Opzione", + "Options": "Opzioni", "Photo": "Foto", "Photos and Videos": "Foto e Video", "Pin to Conversation": "Metti in evidenza", @@ -59,28 +70,51 @@ "Please allow Audio permissions in settings.": "Si prega di consentire le autorizzazioni audio nelle impostazioni.", "Please enable access to your photos and videos so you can share them.": "Abilita l'accesso alle tue foto e ai tuoi video in modo da poterli condividere.", "Please select a channel first": "Seleziona un canale", + "Poll Comments": "Commenti sul sondaggio", + "Poll Options": "Opzioni del sondaggio", + "Poll Results": "Risultati del sondaggio", + "Questions": "Domande", "Reconnecting...": "Ricollegarsi...", "Reply": "Rispondi", "Reply to Message": "Rispondi al messaggio", "Resend": "Invia di nuovo", + "SEND": "INVIA", "Search GIFs": "Cerca GIF", + "See all {{count}} options_many": "Vedi tutte le {{count}} opzioni", + "See all {{count}} options_one": "Vedi tutte le {{count}} opzioni", + "See all {{count}} options_other": "Vedi tutte le {{count}} opzioni", "Select More Photos": "Seleziona Altre foto", + "Select one": "Seleziona una", + "Select one or more": "Seleziona una o più", + "Select up to {{count}}_many": "Seleziona fino a {{count}}", + "Select up to {{count}}_one": "Seleziona fino a {{count}}", + "Select up to {{count}}_other": "Seleziona fino a {{count}}", "Send Anyway": "Invia comunque", "Send a message": "Mandare un messaggio", "Sending links is not allowed in this conversation": "L'invio di link non è consentito in questa conversazione", + "Show All": "Mostra tutto", "Slow mode ON": "Slowmode attiva", + "Suggest an option": "Suggerisci un'opzione", "The message has been reported to a moderator.": "Il messaggio è stato segnalato a un moderatore.", "The source message was deleted": "Il messaggio originale è stato eliminato", + "This is already an option": "Questa è già un'opzione", "This reply was deleted": "Questa risposta è stata eliminata", "Thread Reply": "Rispondi alla Discussione", + "Type a number from 2 to 10": "Digita un numero da 2 a 10", "Unban User": "Sblocca Utente", "Unblock User": "Sblocca utente", "Unknown User": "Utente sconosciuto", "Unmute User": "Riattiva utente", "Unpin from Conversation": "Rimuovi dagli elementi in evidenza", "Unread Messages": "Messaggi non letti", + "Update your comment": "Aggiorna il tuo commento", "Video": "Video", + "View Results": "Visualizza i risultati", + "View {{count}} comments_many": "Vedi {{count}} commenti", + "View {{count}} comments_one": "Vedi {{count}} commento", + "View {{count}} comments_other": "Vedi {{count}} commenti", "Voice message": "Messaggio vocale", + "Vote ended": "Votazione terminata", "You": "Tu", "You can't send messages in this channel": "Non puoi inviare messaggi in questo canale", "replied to": "ha risposto a", @@ -90,6 +124,7 @@ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}", + "timestamp/PollVote": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Ieri]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Domani]\",\"nextWeek\":\"dddd [alle] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Ieri]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Domani]\",\"nextWeek\":\"dddd [alle] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} e altri {{ nonSelfUserLength }} stanno scrivendo", @@ -97,5 +132,8 @@ "{{ replyCount }} Replies": "{{ replyCount }} Risposte", "{{ replyCount }} Thread Replies": "{{replyCount}} Risposte alle Conversazione", "{{ user }} is typing": "{{ user }} sta scrivendo", + "{{count}} votes_many": "{{count}} voti", + "{{count}} votes_one": "{{count}} voto", + "{{count}} votes_other": "{{count}} voti", "🏙 Attachment...": "🏙 Allegato..." } diff --git a/package/src/i18n/ja.json b/package/src/i18n/ja.json index f4c21ca25e..cd3a540baf 100644 --- a/package/src/i18n/ja.json +++ b/package/src/i18n/ja.json @@ -1,17 +1,23 @@ { "1 Reply": "1件の返信", "1 Thread Reply": "1件のスレッド返信", + "Add a comment": "コメントを追加", + "Add an option": "オプションを追加", "Allow access to your Gallery": "ギャラリーへのアクセスを許可する", "Allow camera access in device settings": "デバイス設定でカメラへのアクセスを許可する", "Also send to channel": "チャンネルにも送信", + "Anonymous": "匿名", + "Anonymous poll": "匿名アンケート", "Are you sure you want to permanently delete this message?": "このメッセージを完全に削除してもよろしいですか?", "Are you sure?": "本当によろしいですか?", + "Ask a question": "質問をする", "Ban User": "ユーザーを禁止する", "Block User": "ユーザをブロックする", "Cancel": "キャンセル", "Cannot Flag Message": "メッセージをフラグできません", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "あなたのコメントが他の人にどのように影響するか考え、必ずコミュニティガイドラインに従ってください", "Copy Message": "メッセージのコピー", + "Create Poll": "アンケートを作成", "Delete": "消去", "Delete Message": "メッセージを削除", "Device camera is used to take photos or videos.": "デバイスのカメラは写真やビデオの撮影に使用されます。", @@ -22,6 +28,7 @@ "Editing Message": "メッセージを編集中", "Emoji matching": "絵文字マッチング", "Empty message...": "空のメッセージ...", + "End Vote": "投票を終了", "Error loading": "読み込みエラー", "Error loading channel list...": "チャネルリストの読み込み中にエラーが発生しました。。。", "Error loading messages for this channel...": "このチャネルのメッセージの読み込み中にエラーが発生しました。。。", @@ -41,9 +48,11 @@ "Loading threads...": "スレッドを読み込み中...", "Loading...": "読み込み中。。。", "Maximum number of files reached": "ファイルの最大数に達しました", + "Maximum votes per person": "1人あたりの最大投票数", "Message Reactions": "メッセージのリアクション", "Message deleted": "メッセージが削除されました", "Message flagged": "メッセージにフラグが付けられました", + "Multiple answers": "複数回答", "Mute User": "ユーザーをミュートする", "No chats here yet…": "まだチャットはありません…", "No threads here yet": "まだスレッドがありません", @@ -52,6 +61,8 @@ "Ok": "確認", "Only visible to you": "あなただけに見える", "Open Settings": "設定を開く", + "Option": "オプション", + "Options": "オプション", "Photo": "写真", "Photos and Videos": "写真と動画", "Pin to Conversation": "会話にピンする", @@ -59,28 +70,48 @@ "Please allow Audio permissions in settings.": "設定でオーディオの権限を許可してください。", "Please enable access to your photos and videos so you can share them.": "写真やビデオへのアクセスを有効にして、共有できるようにしてください。", "Please select a channel first": "最初にチャンネルを選択してください", + "Poll Comments": "アンケートのコメント", + "Poll Options": "アンケートのオプション", + "Poll Results": "アンケートの結果", + "Questions": "質問", "Reconnecting...": "再接続中。。。", "Reply": "返事", "Reply to Message": "メッセージに返信", "Resend": "再送", + "SEND": "送信", "Search GIFs": "GIFの探索", + "See all {{count}} options_one": "{{count}} 個のオプションをすべて表示", + "See all {{count}} options_other": "{{count}} 個のオプションをすべて表示", "Select More Photos": "さらに写真を選択", + "Select one": "1つ選択", + "Select one or more": "1つ以上選択", + "Select up to {{count}}_one": "{{count}} まで選択", + "Select up to {{count}}_other": "{{count}} まで選択", "Send Anyway": "とにかく送信", "Send a message": "メッセージを送る", "Sending links is not allowed in this conversation": "この会話ではリンク機能を使用できません。", + "Show All": "すべて表示", "Slow mode ON": "スローモードオン", + "Suggest an option": "オプションを提案", "The message has been reported to a moderator.": "メッセージはモデレーターに報告されました。", "The source message was deleted": "元のメッセージが削除されました", + "This is already an option": "これはすでにオプションです", "This reply was deleted": "この返信は削除されました", "Thread Reply": "スレッドの返信", + "Type a number from 2 to 10": "2から10の数字を入力してください", "Unban User": "ユーザーの禁止を解除する", "Unblock User": "ユーザーのブロックを解除する", "Unknown User": "不明なユーザー", "Unmute User": "ユーザーのミュートを解除する", "Unpin from Conversation": "会話のピンを外す", "Unread Messages": "未読メッセージ", + "Update your comment": "コメントを更新", "Video": "ビデオ", + "View Results": "結果を表示", + "View {{count}} comments_one": "{{count}} 件のコメントを表示", + "View {{count}} comments_other": "{{count}} 件のコメントを表示", "Voice message": "ボイスメッセージ", + "Vote ended": "投票終了", "You": "あなた", "You can't send messages in this channel": "このチャンネルではメッセージを送信できません", "replied to": "に返信しました", @@ -90,6 +121,7 @@ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}", + "timestamp/PollVote": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[昨日]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[明日]\",\"nextWeek\":\"dddd [の] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[昨日]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[明日]\",\"nextWeek\":\"dddd [の] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }}と{{ nonSelfUserLength }}人がタイピングしています", @@ -97,5 +129,7 @@ "{{ replyCount }} Replies": "{{ replyCount }}件の返信", "{{ replyCount }} Thread Replies": "{{ replyCount }}件のスレッド返信", "{{ user }} is typing": "{{ user }}はタイピング中", + "{{count}} votes_one": "{{count}} 票", + "{{count}} votes_other": "{{count}} 票", "🏙 Attachment...": "🏙 アタッチメント..." } diff --git a/package/src/i18n/ko.json b/package/src/i18n/ko.json index d6deb779a1..5ab6624773 100644 --- a/package/src/i18n/ko.json +++ b/package/src/i18n/ko.json @@ -1,17 +1,23 @@ { "1 Reply": "답장 1개", "1 Thread Reply": "1개의 스레드\u3000답글", + "Add a comment": "댓글 추가", + "Add an option": "옵션 추가", "Allow access to your Gallery": "갤러리에 대한 액세스를 허용", "Allow camera access in device settings": "기기 설정에서 카메라 액세스를 허용하세요.", "Also send to channel": "채널에도 전송", + "Anonymous": "익명", + "Anonymous poll": "익명 투표", "Are you sure you want to permanently delete this message?": "이 메시지를 영구적으로 삭제하시겠습니까?", "Are you sure?": "확실합니까?", + "Ask a question": "질문하기", "Ban User": "사용자 차단", "Block User": "사용자 차단", "Cancel": "취소", "Cannot Flag Message": "메세지를 플래그 할 수 없습니다", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "당신의 댓글이 다른 사람들에게 어떤 영향을 줄지 고려하고 반드시 우리의 커뮤니티 가이드라인을 따르십시오", "Copy Message": "메시지 복사", + "Create Poll": "투표 생성", "Delete": "삭제", "Delete Message": "메시지 삭제", "Device camera is used to take photos or videos.": "기기 카메라는 사진이나 동영상을 촬영하는 데 사용됩니다.", @@ -22,6 +28,7 @@ "Editing Message": "메시지 편집중", "Emoji matching": "이모티콘 매칭", "Empty message...": "빈 메시지...", + "End Vote": "투표 종료", "Error loading": "로드 오류", "Error loading channel list...": "채널리스트 을로드하는 동안 오류가 발생했습니다...", "Error loading messages for this channel...": "이 채널의 메시지를로드하는 동안 오류가 발생했습니다...", @@ -41,9 +48,11 @@ "Loading threads...": "스레드 로딩 중...", "Loading...": "로딩 중...", "Maximum number of files reached": "최대 파일 수에 도달했습니다", + "Maximum votes per person": "사람당 최대 투표 수", "Message Reactions": "메시지의 리액션", "Message deleted": "메시지가 삭제되었습니다.", "Message flagged": "메시지에 플래그가 지정되었습니다", + "Multiple answers": "다중 답변", "Mute User": "사용자를 음소거", "No chats here yet…": "아직 여기에 채팅이 없어요…", "No threads here yet": "아직 스레드가 없습니다", @@ -52,6 +61,8 @@ "Ok": "확인", "Only visible to you": "당신만 볼 수 있습니다", "Open Settings": "설정 열기", + "Option": "옵션", + "Options": "옵션", "Photo": "사진", "Photos and Videos": "사진과 동영상", "Pin to Conversation": "대화에 고정합니다", @@ -59,28 +70,48 @@ "Please allow Audio permissions in settings.": "설정에서 오디오 권한을 허용해주세요.", "Please enable access to your photos and videos so you can share them.": "사진 및 비디오에 대한 액세스를 사용하여 공유 할 수 있도록합니다.", "Please select a channel first": "먼저 채널을 선택하십시오", + "Poll Comments": "투표 댓글", + "Poll Options": "투표 옵션", + "Poll Results": "투표 결과", + "Questions": "질문", "Reconnecting...": "다시 연결 중...", "Reply": "답장", "Reply to Message": "메시지에 답장", "Resend": "재전송", + "SEND": "보내기", "Search GIFs": "GIF의 검색", + "See all {{count}} options_one": "모든 {{count}} 옵션 보기", + "See all {{count}} options_other": "모든 {{count}} 옵션 보기", "Select More Photos": "추가 사진 선택", + "Select one": "하나 선택", + "Select one or more": "하나 이상 선택", + "Select up to {{count}}_one": "{{count}}까지 선택", + "Select up to {{count}}_other": "{{count}}까지 선택", "Send Anyway": "그래도 보내기", "Send a message": "메세지를 보내다", "Sending links is not allowed in this conversation": "이 대화에서는 링크 기능을 사용할 수 없습니다", + "Show All": "모두 보기", "Slow mode ON": "슬로모드 켜짐", + "Suggest an option": "옵션 제안", "The message has been reported to a moderator.": "메시지는 운영자에보고되었습니다.", "The source message was deleted": "원본 메시지가 삭제되었습니다", + "This is already an option": "이미 존재하는 옵션입니다", "This reply was deleted": "이 답글은 삭제되었습니다", "Thread Reply": "스레드\u3000답장", + "Type a number from 2 to 10": "2에서 10 사이의 숫자를 입력하세요", "Unban User": "사용자 차단 해제", "Unblock User": "사용자 차단 해제", "Unknown User": "알 수없는 사용자", "Unmute User": "사용자 음소거 해제", "Unpin from Conversation": "대화의 핀을 분리합니다", "Unread Messages": "읽지 않은 메시지", + "Update your comment": "댓글 수정", "Video": "동영상", + "View Results": "결과 보기", + "View {{count}} comments_one": "{{count}}개의 댓글 보기", + "View {{count}} comments_other": "{{count}}개의 댓글 보기", "Voice message": "음성 메시지", + "Vote ended": "투표 종료됨", "You": "당신", "You can't send messages in this channel": "이 채널에서는 메세지를 전송할 수 없습니다", "replied to": "에 답장했습니다", @@ -90,6 +121,7 @@ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}", + "timestamp/PollVote": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[어제]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[내일]\",\"nextWeek\":\"dddd [LT에]\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[어제]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[내일]\",\"nextWeek\":\"dddd [LT에]\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} 외 {{ nonSelfUserLength }}명이 입력 중입니다", @@ -97,5 +129,7 @@ "{{ replyCount }} Replies": "{{ replyCount }} 답글", "{{ replyCount }} Thread Replies": "{{ replyCount }}\u3000스레드\u3000답글", "{{ user }} is typing": "{{ user }} 타이핑 중", + "{{count}} votes_one": "{{count}} 표", + "{{count}} votes_other": "{{count}} 표", "🏙 Attachment...": "🏙 부착..." } diff --git a/package/src/i18n/nl.json b/package/src/i18n/nl.json index 40e4b233fb..258c6dbf2f 100644 --- a/package/src/i18n/nl.json +++ b/package/src/i18n/nl.json @@ -1,17 +1,23 @@ { "1 Reply": "1 Antwoord", "1 Thread Reply": "1 thread antwoord", + "Add a comment": "Voeg een reactie toe", + "Add an option": "Voeg een optie toe", "Allow access to your Gallery": "Geef toegang tot uw galerij", "Allow camera access in device settings": "Sta cameratoegang toe in de apparaatinstellingen", "Also send to channel": "Stuur ook naar kanaal", + "Anonymous": "Anoniem", + "Anonymous poll": "Anonieme peiling", "Are you sure you want to permanently delete this message?": "Weet u zeker dat u dit bericht definitief wilt verwijderen?", "Are you sure?": "Weet je het zeker?", + "Ask a question": "Stel een vraag", "Ban User": "Gebruiker Verbannen", "Block User": "Blokkeer Gebruiker", "Cancel": "Annuleer", "Cannot Flag Message": "Kan bericht niet rapporteren", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "Denk na over hoe jouw opmerking anderen zou kunnen laten voelen en zorg ervoor dat je onze Community-richtlijnen volgt", "Copy Message": "Bericht kopiëren", + "Create Poll": "Peiling aanmaken", "Delete": "Verwijderen", "Delete Message": "Verwijder bericht", "Device camera is used to take photos or videos.": "De camera van het apparaat wordt gebruikt om foto's of video's te maken.", @@ -22,6 +28,7 @@ "Editing Message": "Bericht aanpassen", "Emoji matching": "Emoji-overeenkomsten", "Empty message...": "Leeg bericht...", + "End Vote": "Einde stemronde", "Error loading": "Probleem bij het laden", "Error loading channel list...": "Probleem bij het laden van de kanalen...", "Error loading messages for this channel...": "Probleem bij het laden van de berichten in dit kanaal...", @@ -41,9 +48,11 @@ "Loading threads...": "Threads laden...", "Loading...": "Aan het laden...", "Maximum number of files reached": "Maximaal aantal bestanden bereikt", + "Maximum votes per person": "Maximaal aantal stemmen per persoon", "Message Reactions": "Bericht Reacties", "Message deleted": "Bericht verwijderd", "Message flagged": "Bericht gemarkeerd", + "Multiple answers": "Meerdere antwoorden", "Mute User": "Gebruiker dempen", "No chats here yet…": "Nog geen chats hier…", "No threads here yet": "Hier zijn nog geen threads", @@ -52,6 +61,8 @@ "Ok": "Oké", "Only visible to you": "Alleen zichtbaar voor jou", "Open Settings": "Open instellingen", + "Option": "Optie", + "Options": "Opties", "Photo": "Foto", "Photos and Videos": "Foto's en video's", "Pin to Conversation": "Vastmaken aan gesprek", @@ -59,28 +70,48 @@ "Please allow Audio permissions in settings.": "Gelieve audio toestemmingen toe te staan in de instellingen.", "Please enable access to your photos and videos so you can share them.": "Schakel toegang tot uw foto's en video's in zodat u ze kunt delen.", "Please select a channel first": "Selecteer eerst een kanaal", + "Poll Comments": "Peiling reacties", + "Poll Options": "Peiling opties", + "Poll Results": "Peiling resultaten", + "Questions": "Vragen", "Reconnecting...": "Opnieuw Verbinding Maken...", "Reply": "Antwoord", "Reply to Message": "Beantwoord bericht", "Resend": "Opnieuw versturen", + "SEND": "VERZENDEN", "Search GIFs": "Zoek GIF's", + "See all {{count}} options_one": "Bekijk alle {{count}} opties", + "See all {{count}} options_other": "Bekijk alle {{count}} opties", "Select More Photos": "Selecteer Meer foto's", + "Select one": "Kies één", + "Select one or more": "Kies één of meer", + "Select up to {{count}}_one": "Kies tot {{count}}", + "Select up to {{count}}_other": "Kies tot {{count}}", "Send Anyway": "Toch verzenden", "Send a message": "Stuur een bericht", "Sending links is not allowed in this conversation": "In dit gesprek is het niet toegestaan links te versturen", + "Show All": "Alles weergeven", "Slow mode ON": "Langzame modus aan", + "Suggest an option": "Stel een optie voor", "The message has been reported to a moderator.": "Het bericht is gerapporteerd aan een moderator.", "The source message was deleted": "Het oorspronkelijke bericht is verwijderd", + "This is already an option": "Dit is al een optie", "This reply was deleted": "Deze reactie is verwijderd", "Thread Reply": "Discussie beantwoorden", + "Type a number from 2 to 10": "Typ een getal van 2 tot 10", "Unban User": "Gebruiker Deblokeren", "Unblock User": "Deblokkeer gebruiker", "Unknown User": "Onbekende gebruiker", "Unmute User": "Dempen van gebruiker opheffen", "Unpin from Conversation": "Losmaken van gesprek", "Unread Messages": "Ongelezen Berichten", + "Update your comment": "Werk je reactie bij", "Video": "Video", + "View Results": "Bekijk resultaten", + "View {{count}} comments_one": "Bekijk {{count}} reactie", + "View {{count}} comments_other": "Bekijk {{count}} reacties", "Voice message": "Spraakbericht", + "Vote ended": "Stemmen beëindigd", "You": "U", "You can't send messages in this channel": "Je kan geen berichten sturen in dit kanaal", "replied to": "reageerde op", @@ -90,6 +121,7 @@ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}", + "timestamp/PollVote": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Gisteren]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Morgen]\",\"nextWeek\":\"dddd [om] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Gisteren]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Morgen]\",\"nextWeek\":\"dddd [om] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} en {{ nonSelfUserLength }} anderen zijn aan het typen", @@ -97,5 +129,7 @@ "{{ replyCount }} Replies": "{{ replyCount }} Antwoorden", "{{ replyCount }} Thread Replies": "{{replyCount}} Discussiereacties", "{{ user }} is typing": "{{ user }} is aan het typen", + "{{count}} votes_one": "{{count}} stem", + "{{count}} votes_other": "{{count}} stemmen", "🏙 Attachment...": "🏙 Bijlage..." } diff --git a/package/src/i18n/pt-br.json b/package/src/i18n/pt-br.json index e54242e63f..2d4ff45565 100644 --- a/package/src/i18n/pt-br.json +++ b/package/src/i18n/pt-br.json @@ -1,17 +1,23 @@ { "1 Reply": "1 Resposta", "1 Thread Reply": "1 Resposta de Thread", + "Add a comment": "Adicionar um comentário", + "Add an option": "Adicionar uma opção", "Allow access to your Gallery": "Permitir acesso à sua Galeria", "Allow camera access in device settings": "Permitir acesso à câmera nas configurações do dispositivo", "Also send to channel": "Também enviar para o canal", + "Anonymous": "Anônimo", + "Anonymous poll": "Enquete anônima", "Are you sure you want to permanently delete this message?": "Tem certeza de que deseja excluir esta mensagem permanentemente?", "Are you sure?": "Tem certeza?", + "Ask a question": "Fazer uma pergunta", "Ban User": "Banir Usuário", "Block User": "Bloquear Usuário", "Cancel": "Cancelar", "Cannot Flag Message": "Não é possível reportar a mensagem", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "Considere como seu comentário pode fazer os outros se sentirem e certifique-se de seguir nossas Diretrizes da Comunidade", "Copy Message": "Copiar Mensagem", + "Create Poll": "Criar enquete", "Delete": "Excluir", "Delete Message": "Excluir Mensagem", "Device camera is used to take photos or videos.": "A câmera do dispositivo é usada para tirar fotos ou vídeos.", @@ -22,6 +28,7 @@ "Editing Message": "Editando Mensagem", "Emoji matching": "Correspondência de Emoji", "Empty message...": "Mensagem vazia...", + "End Vote": "Encerrar votação", "Error loading": "Erro ao carregar", "Error loading channel list...": "Erro ao carregar lista de canais...", "Error loading messages for this channel...": "Erro ao carregar mensagens para este canal...", @@ -41,9 +48,11 @@ "Loading threads...": "Carregando tópicos...", "Loading...": "Carregando...", "Maximum number of files reached": "Número máximo de arquivos atingido", + "Maximum votes per person": "Máximo de votos por pessoa", "Message Reactions": "Reações à Mensagem", "Message deleted": "Mensagem excluída", "Message flagged": "Mensagem sinalizada", + "Multiple answers": "Múltiplas respostas", "Mute User": "Silenciar Usuário", "No chats here yet…": "Ainda não há chats aqui...", "No threads here yet": "Ainda não há tópicos aqui", @@ -52,6 +61,8 @@ "Ok": "Ok", "Only visible to you": "Apenas visível para você", "Open Settings": "Abrir Configurações", + "Option": "Opção", + "Options": "Opções", "Photo": "Foto", "Photos and Videos": "Fotos e Vídeos", "Pin to Conversation": "Fixar na Conversa", @@ -59,28 +70,51 @@ "Please allow Audio permissions in settings.": "Por favor, permita as permissões de áudio nas configurações.", "Please enable access to your photos and videos so you can share them.": "Por favor, habilite o acesso às suas fotos e vídeos para poder compartilhá-los.", "Please select a channel first": "Por favor, selecione um canal primeiro", + "Poll Comments": "Comentários da enquete", + "Poll Options": "Opções da enquete", + "Poll Results": "Resultados da enquete", + "Questions": "Perguntas", "Reconnecting...": "Reconectando...", "Reply": "Responder", "Reply to Message": "Responder à Mensagem", "Resend": "Reenviar", + "SEND": "ENVIAR", "Search GIFs": "Pesquisar GIFs", + "See all {{count}} options_many": "Ver todas as {{count}} opções", + "See all {{count}} options_one": "Ver todas as {{count}} opções", + "See all {{count}} options_other": "Ver todas as {{count}} opções", "Select More Photos": "Selecionar Mais Fotos", + "Select one": "Selecione uma", + "Select one or more": "Selecione uma ou mais", + "Select up to {{count}}_many": "Selecione até {{count}}", + "Select up to {{count}}_one": "Selecione até {{count}}", + "Select up to {{count}}_other": "Selecione até {{count}}", "Send Anyway": "Enviar de qualquer maneira", "Send a message": "Enviar uma mensagem", "Sending links is not allowed in this conversation": "Não é permitido enviar links nesta conversa", + "Show All": "Mostrar tudo", "Slow mode ON": "Modo Lento ATIVADO", + "Suggest an option": "Sugerir uma opção", "The message has been reported to a moderator.": "A mensagem foi relatada a um moderador.", "The source message was deleted": "A mensagem original foi excluída", + "This is already an option": "Isso já é uma opção", "This reply was deleted": "Esta resposta foi excluída", "Thread Reply": "Respostas de Tópico", + "Type a number from 2 to 10": "Digite um número de 2 a 10", "Unban User": "Desbanir Usuário", "Unblock User": "Desbloquear Usuário", "Unknown User": "Usuário Desconhecido", "Unmute User": "Remover usuário do modo silencioso", "Unpin from Conversation": "Desmarcar como fixado na conversa", "Unread Messages": "Mensagens não lidas", + "Update your comment": "Atualize seu comentário", "Video": "Vídeo", + "View Results": "Ver resultados", + "View {{count}} comments_many": "Ver {{count}} comentários", + "View {{count}} comments_one": "Ver {{count}} comentário", + "View {{count}} comments_other": "Ver {{count}} comentários", "Voice message": "Mensagem de voz", + "Vote ended": "Votação encerrada", "You": "Você", "You can't send messages in this channel": "Você não pode enviar mensagens neste canal", "replied to": "respondeu a", @@ -90,6 +124,7 @@ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}", + "timestamp/PollVote": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Ontem]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Amanhã]\",\"nextWeek\":\"dddd [às] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Ontem]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Amanhã]\",\"nextWeek\":\"dddd [às] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} e mais {{ nonSelfUserLength }} pessoa(s) estão digitando", @@ -97,5 +132,8 @@ "{{ replyCount }} Replies": "{{ replyCount }} Respostas", "{{ replyCount }} Thread Replies": "{{ replyCount }} Respostas de Thread", "{{ user }} is typing": "{{ user }} está digitando", + "{{count}} votes_many": "{{count}} votos", + "{{count}} votes_one": "{{count}} voto", + "{{count}} votes_other": "{{count}} votos", "🏙 Attachment...": "🏙 Anexo..." } diff --git a/package/src/i18n/ru.json b/package/src/i18n/ru.json index d2947987bd..fa2f946ea2 100644 --- a/package/src/i18n/ru.json +++ b/package/src/i18n/ru.json @@ -1,17 +1,23 @@ { "1 Reply": "1 Ответ", "1 Thread Reply": "1 тема Ответить", + "Add a comment": "Добавить комментарий", + "Add an option": "Добавить вариант", "Allow access to your Gallery": "Разрешить доступ к вашей галерее", "Allow camera access in device settings": "Разрешите доступ к камере в настройках устройства.", "Also send to channel": "Также отправить на канал", + "Anonymous": "Анонимный", + "Anonymous poll": "Анонимный опрос", "Are you sure you want to permanently delete this message?": "Вы действительно хотите удалить это сообщение без возможности восстановления?", "Are you sure?": "Вы уверены?", + "Ask a question": "Задайте вопрос", "Ban User": "Заблокировать Пользователя", "Block User": "Заблокировать пользователя", "Cancel": "Отмена", "Cannot Flag Message": "Невозможно пожаловаться на сообщение", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "Обдумайте, как ваш комментарий может повлиять на других, и убедитесь, что вы следуете нашим правилам сообщества", "Copy Message": "Копировать сообщение", + "Create Poll": "Создать опрос", "Delete": "удалять", "Delete Message": "Удалить сообщение", "Device camera is used to take photos or videos.": "Камера устройства используется для съемки фотографий или видео.", @@ -22,6 +28,7 @@ "Editing Message": "Редактирование сообщения", "Emoji matching": "Соответствие эмодзи", "Empty message...": "Пустое сообщение...", + "End Vote": "Завершить голосование", "Error loading": "Ошибка при загрузке", "Error loading channel list...": "Ошибка загрузки списка каналов...", "Error loading messages for this channel...": "Ошибка загрузки сообщений для этого канала...", @@ -41,9 +48,11 @@ "Loading threads...": "Загрузка потоков...", "Loading...": "Загружаю...", "Maximum number of files reached": "Достигнуто максимальное количество файлов", + "Maximum votes per person": "Максимальное количество голосов на человека", "Message Reactions": "Сообщения Реакции", "Message deleted": "Сообщение удалено", "Message flagged": "Сообщение отмечено", + "Multiple answers": "Несколько ответов", "Mute User": "Отключить пользователя", "No chats here yet…": "Здесь пока нет чатов…", "No threads here yet": "Здесь пока нет потоков", @@ -52,6 +61,8 @@ "Ok": "Oк", "Only visible to you": "Видно только вам", "Open Settings": "Открыть настройки", + "Option": "Вариант", + "Options": "Варианты", "Photo": "Фото", "Photos and Videos": "Фото и видео", "Pin to Conversation": "Закрепить к беседе", @@ -59,28 +70,54 @@ "Please allow Audio permissions in settings.": "Пожалуйста, разрешите разрешения на аудио в настройках.", "Please enable access to your photos and videos so you can share them.": "Разрешите доступ к своим фотографиям и видео, чтобы вы могли ими поделиться.", "Please select a channel first": "Пожалуйста, сначала выберите канал", + "Poll Comments": "Комментарии к опросу", + "Poll Options": "Варианты опроса", + "Poll Results": "Результаты опроса", + "Questions": "Вопросы", "Reconnecting...": "Переподключение...", "Reply": "Ответить", "Reply to Message": "Ответить на сообщение", "Resend": "Отправить", + "SEND": "ОТПРАВИТЬ", "Search GIFs": "Поиск GIF", + "See all {{count}} options_few": "Посмотреть все {{count}} вариантов", + "See all {{count}} options_many": "Посмотреть все {{count}} вариантов", + "See all {{count}} options_one": "Посмотреть все {{count}} вариант", + "See all {{count}} options_other": "Посмотреть все {{count}} вариантов", "Select More Photos": "Выбрать больше фотографий", + "Select one": "Выберите один", + "Select one or more": "Выберите один или несколько", + "Select up to {{count}}_few": "Выберите до {{count}}", + "Select up to {{count}}_many": "Выберите до {{count}}", + "Select up to {{count}}_one": "Выберите до {{count}}", + "Select up to {{count}}_other": "Выберите до {{count}}", "Send Anyway": "Всё равно отправить", "Send a message": "Отправить сообщение", "Sending links is not allowed in this conversation": "Отправка ссылок недоступна в этом чате", + "Show All": "Показать все", "Slow mode ON": "Медленный режим включен", + "Suggest an option": "Предложить вариант", "The message has been reported to a moderator.": "Сообщение отправлено модератору.", "The source message was deleted": "Исходное сообщение было удалено", + "This is already an option": "Это уже вариант", "This reply was deleted": "Этот ответ был удалён", "Thread Reply": "Тема Ответить", + "Type a number from 2 to 10": "Введите число от 2 до 10", "Unban User": "Разблокировать Пользователя", "Unblock User": "Разблокировать пользователя", "Unknown User": "Неизвестный пользователь", "Unmute User": "Включить микрофон", "Unpin from Conversation": "Открепить от беседы", "Unread Messages": "Непрочитанные Сообщения", + "Update your comment": "Обновить ваш комментарий", "Video": "видео", + "View Results": "Посмотреть результаты", + "View {{count}} comments_few": "Посмотреть {{count}} комментария", + "View {{count}} comments_many": "Посмотреть {{count}} комментария", + "View {{count}} comments_one": "Посмотреть {{count}} комментарий", + "View {{count}} comments_other": "Посмотреть {{count}} комментария", "Voice message": "Голосовое сообщение", + "Vote ended": "Голосование завершено", "You": "Вы", "You can't send messages in this channel": "Вы не можете отправлять сообщения в этот канал", "replied to": "ответил на", @@ -90,6 +127,7 @@ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}", + "timestamp/PollVote": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Вчера]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Завтра]\",\"nextWeek\":\"dddd [в] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Вчера]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Завтра]\",\"nextWeek\":\"dddd [в] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} и еще {{ nonSelfUserLength }} пишут", @@ -97,5 +135,9 @@ "{{ replyCount }} Replies": "{{ replyCount }} Ответов", "{{ replyCount }} Thread Replies": "{{replyCount}} Ответы в темах", "{{ user }} is typing": "{{ user }} пишет", + "{{count}} votes_few": "{{count}} голосов", + "{{count}} votes_many": "{{count}} голосов", + "{{count}} votes_one": "{{count}} голос", + "{{count}} votes_other": "{{count}} голосов", "🏙 Attachment...": "🏙 Вложение..." } diff --git a/package/src/i18n/tr.json b/package/src/i18n/tr.json index 4c0b62e88b..4493eabbef 100644 --- a/package/src/i18n/tr.json +++ b/package/src/i18n/tr.json @@ -1,17 +1,23 @@ { "1 Reply": "1 Cevap", "1 Thread Reply": "1 Konu Yanıtı", + "Add a comment": "Yorum ekle", + "Add an option": "Seçenek ekle", "Allow access to your Gallery": "Galerinize erişime izin verin", "Allow camera access in device settings": "Cihaz ayarlarında kamera erişimine izin ver", "Also send to channel": "Kanala da gönder", + "Anonymous": "Anonim", + "Anonymous poll": "Anonim anket", "Are you sure you want to permanently delete this message?": "Bu mesajı kalıcı olarak silmek istediğinizden emin misiniz?", "Are you sure?": "Emin misiniz?", + "Ask a question": "Bir soru sor", "Ban User": "Kullanıcıyı Yasakla", "Block User": "Kullanıcıyı engelle", "Cancel": "İptal", "Cannot Flag Message": "Raporlama Başarısız", "Consider how your comment might make others feel and be sure to follow our Community Guidelines": "Yorumunuzun diğerlerini nasıl hissettirebileceğini düşünün ve topluluk kurallarımızı takip ettiğinizden emin olun", "Copy Message": "Mesajı Kopyala", + "Create Poll": "Anket oluştur", "Delete": "Sil", "Delete Message": "Mesajı Sil", "Device camera is used to take photos or videos.": "Cihaz kamerası fotoğraf veya video çekmek için kullanılır.", @@ -22,6 +28,7 @@ "Editing Message": "Mesaj Düzenleniyor", "Emoji matching": "Emoji eşleştirme", "Empty message...": "Boş mesaj...", + "End Vote": "Oylamayı sonlandır", "Error loading": "Yükleme hatası", "Error loading channel list...": "Kanal listesi yüklenirken hata oluştu...", "Error loading messages for this channel...": "Bu kanal için mesajlar yüklenirken hata oluştu...", @@ -41,9 +48,11 @@ "Loading threads...": "Akışlar yükleniyor...", "Loading...": "Yükleniyor...", "Maximum number of files reached": "Maksimum dosya sayısına ulaşıldı", + "Maximum votes per person": "Kişi başına maksimum oy", "Message Reactions": "Mesaj Tepkileri", "Message deleted": "Mesaj silindi", "Message flagged": "Mesaj işaretlendi", + "Multiple answers": "Birden fazla yanıt", "Mute User": "Kullanıcıyı sessize al", "No chats here yet…": "Henüz burada sohbet yok…", "No threads here yet": "Burada henüz akış yok", @@ -52,6 +61,8 @@ "Ok": "Tamam", "Only visible to you": "Sadece siz görebilirsiniz", "Open Settings": "Ayarları aç", + "Option": "Seçenek", + "Options": "Seçenekler", "Photo": "Fotoğraf", "Photos and Videos": "Fotoğraflar ve Videolar", "Pin to Conversation": "Konuşmaya sabitle", @@ -59,28 +70,48 @@ "Please allow Audio permissions in settings.": "Lütfen ayarlarda ses izinlerine izin verin", "Please enable access to your photos and videos so you can share them.": "Paylaşım yapabilmek için lutfen fotoğraflarınıza ve videolarınıza erişimi etkinleştirin.", "Please select a channel first": "Lütfen önce bir kanal seçiniz", + "Poll Comments": "Anket Yorumları", + "Poll Options": "Anket Seçenekleri", + "Poll Results": "Anket Sonuçları", + "Questions": "Sorular", "Reconnecting...": "Yeniden Bağlanılıyor...", "Reply": "Yanıtla", "Reply to Message": "Mesajı Yanıtla", "Resend": "Yeniden gönder", + "SEND": "GÖNDER", "Search GIFs": "GIF Ara", + "See all {{count}} options_one": "Tüm {{count}} seçeneği gör", + "See all {{count}} options_other": "Tüm {{count}} seçeneği gör", "Select More Photos": "Daha Fazla Fotoğraf Seçin", + "Select one": "Birini seç", + "Select one or more": "Bir veya daha fazlasını seç", + "Select up to {{count}}_one": "{{count}} kadar seç", + "Select up to {{count}}_other": "{{count}} kadar seç", "Send Anyway": "Yine de Gönder", "Send a message": "Mesaj gönder", "Sending links is not allowed in this conversation": "Bu konuşmada bağlantı göndermek desteklenmiyor", + "Show All": "Hepsini göster", "Slow mode ON": "Yavaş Mod Açık", + "Suggest an option": "Bir seçenek öner", "The message has been reported to a moderator.": "Mesaj moderatöre bildirildi.", "The source message was deleted": "Kaynak mesaj silindi", + "This is already an option": "Bu zaten bir seçenek", "This reply was deleted": "Bu yanıt silindi", "Thread Reply": "Konu Yanıtı", + "Type a number from 2 to 10": "2 ile 10 arasında bir sayı girin", "Unban User": "Kullanıcının Yasağını Kaldır", "Unblock User": "Kullanıcının engelini kaldır", "Unknown User": "Bilinmeyen kullanıcı", "Unmute User": "Kullanıcının sesini aç", "Unpin from Conversation": "Sabitlemeyi kaldır", "Unread Messages": "Okunmamış Mesajlar", + "Update your comment": "Yorumunu güncelle", "Video": "Video", + "View Results": "Sonuçları görüntüle", + "View {{count}} comments_one": "{{count}} yorumu görüntüle", + "View {{count}} comments_other": "{{count}} yorumu görüntüle", "Voice message": "Sesli mesaj", + "Vote ended": "Oylama sona erdi", "You": "Sen", "You can't send messages in this channel": "Bu konuşmaya mesaj gönderemezsiniz", "replied to": "yanıtladı", @@ -90,6 +121,7 @@ "timestamp/MessageEditedTimestamp": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}", + "timestamp/PollVote": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Dün]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Yarın]\",\"nextWeek\":\"dddd [saat] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "timestamp/StickyHeader": "{{ timestamp | timestampFormatter(calendar: true) }}", "timestamp/ThreadListItem": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\":\"[Dün]\",\"lastWeek\":\"dddd\",\"nextDay\":\"[Yarın]\",\"nextWeek\":\"dddd [saat] LT\",\"sameDay\":\"LT\",\"sameElse\":\"L\"}) }}", "{{ firstUser }} and {{ nonSelfUserLength }} more are typing": "{{ firstUser }} ve {{ nonSelfUserLength }} kişi daha yazıyor", @@ -97,5 +129,7 @@ "{{ replyCount }} Replies": "{{ replyCount }} Cevap", "{{ replyCount }} Thread Replies": "{{responseCount}} Konu Cevapı", "{{ user }} is typing": "{{ user }} yazıyor", + "{{count}} votes_one": "{{count}} oy", + "{{count}} votes_other": "{{count}} oy", "🏙 Attachment...": "🏙 Ek..." } diff --git a/package/src/icons/Back.tsx b/package/src/icons/Back.tsx new file mode 100644 index 0000000000..e90062e107 --- /dev/null +++ b/package/src/icons/Back.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +import { IconProps, RootPath, RootSvg } from './utils/base'; + +export const Back = (props: IconProps) => ( + + + +); diff --git a/package/src/icons/DragHandle.tsx b/package/src/icons/DragHandle.tsx new file mode 100644 index 0000000000..245e9c049d --- /dev/null +++ b/package/src/icons/DragHandle.tsx @@ -0,0 +1,9 @@ +import React from 'react'; + +import { IconProps, RootPath, RootSvg } from './utils/base'; + +export const DragHandle = (props: IconProps) => ( + + + +); diff --git a/package/src/icons/PollThumbnail.tsx b/package/src/icons/PollThumbnail.tsx new file mode 100644 index 0000000000..1eec10add2 --- /dev/null +++ b/package/src/icons/PollThumbnail.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +import { IconProps, RootPath, RootSvg } from './utils/base'; + +export const PollThumbnail = (props: IconProps) => ( + + + +); diff --git a/package/src/icons/SendPoll.tsx b/package/src/icons/SendPoll.tsx new file mode 100644 index 0000000000..d134556d19 --- /dev/null +++ b/package/src/icons/SendPoll.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +import { IconProps, RootPath, RootSvg } from './utils/base'; + +export const SendPoll = (props: IconProps) => ( + + + +); diff --git a/package/src/icons/index.ts b/package/src/icons/index.ts index 1f8d585808..a82fda783f 100644 --- a/package/src/icons/index.ts +++ b/package/src/icons/index.ts @@ -96,3 +96,7 @@ export * from './ZIP'; export * from './MessageBubble'; export * from './MessageBubbleEmpty'; export * from './Reload'; +export * from './PollThumbnail'; +export * from './DragHandle'; +export * from './Back'; +export * from './SendPoll'; diff --git a/package/src/store/QuickSqliteClient.ts b/package/src/store/QuickSqliteClient.ts index 7cfc8e708b..6d07a8e34c 100644 --- a/package/src/store/QuickSqliteClient.ts +++ b/package/src/store/QuickSqliteClient.ts @@ -30,7 +30,7 @@ import type { PreparedQueries, Table } from './types'; * */ export class QuickSqliteClient { - static dbVersion = 5; + static dbVersion = 6; static dbName = DB_NAME; static dbLocation = DB_LOCATION; diff --git a/package/src/store/apis/index.ts b/package/src/store/apis/index.ts index 1d0dbc913e..ae2740705a 100644 --- a/package/src/store/apis/index.ts +++ b/package/src/store/apis/index.ts @@ -21,3 +21,4 @@ export * from './upsertUserSyncStatus'; export * from './upsertMembers'; export * from './upsertMessages'; export * from './upsertReads'; +export * from './updatePollMessage'; diff --git a/package/src/store/apis/updatePollMessage.ts b/package/src/store/apis/updatePollMessage.ts new file mode 100644 index 0000000000..c3b8c04695 --- /dev/null +++ b/package/src/store/apis/updatePollMessage.ts @@ -0,0 +1,48 @@ +import type { PollResponse } from 'stream-chat'; + +import { QuickSqliteClient } from '../QuickSqliteClient'; +import { createSelectQuery } from '../sqlite-utils/createSelectQuery'; +import { createUpdateQuery } from '../sqlite-utils/createUpdateQuery'; +import type { PreparedQueries } from '../types'; + +export const updatePollMessage = ({ + flush = true, + poll, +}: { + poll: PollResponse; + flush?: boolean; +}) => { + const queries: PreparedQueries[] = []; + + const messagesWithPoll = QuickSqliteClient.executeSql.apply( + null, + createSelectQuery('messages', ['*'], { + poll_id: poll.id, + }), + ); + + for (const message of messagesWithPoll) { + const storablePoll = JSON.stringify({ + ...poll, + latest_votes: message.poll.latest_votes, + own_votes: message.poll.own_votes, + }); + const storableMessage = { ...message, poll: storablePoll }; + + queries.push( + createUpdateQuery('messages', storableMessage, { + id: message.id, + }), + ); + QuickSqliteClient.logger?.('info', 'updatePoll', { + message: storableMessage, + poll: storablePoll, + }); + } + + if (flush) { + QuickSqliteClient.executeSqlBatch(queries); + } + + return queries; +}; diff --git a/package/src/store/mappers/mapMessageToStorable.ts b/package/src/store/mappers/mapMessageToStorable.ts index 9e267115fe..3caac21ba4 100644 --- a/package/src/store/mappers/mapMessageToStorable.ts +++ b/package/src/store/mappers/mapMessageToStorable.ts @@ -18,6 +18,8 @@ export const mapMessageToStorable = ( message_text_updated_at, // eslint-disable-next-line @typescript-eslint/no-unused-vars own_reactions, + poll, + poll_id, reaction_groups, text, type, @@ -34,6 +36,8 @@ export const mapMessageToStorable = ( extraData: JSON.stringify(extraData), id, messageTextUpdatedAt: mapDateTimeToStorable(message_text_updated_at), + poll: JSON.stringify(poll), + poll_id: poll_id || '', reactionGroups: JSON.stringify(reaction_groups), text, type, diff --git a/package/src/store/mappers/mapStorableToMessage.ts b/package/src/store/mappers/mapStorableToMessage.ts index 2d58ad1f4d..b1ae3fa01a 100644 --- a/package/src/store/mappers/mapStorableToMessage.ts +++ b/package/src/store/mappers/mapStorableToMessage.ts @@ -24,6 +24,8 @@ export const mapStorableToMessage = < deletedAt, extraData, messageTextUpdatedAt, + poll, + poll_id, reactionGroups, updatedAt, user, @@ -42,6 +44,8 @@ export const mapStorableToMessage = < latest_reactions: latestReactions, message_text_updated_at: messageTextUpdatedAt, own_reactions: ownReactions, + poll: JSON.parse(poll), + poll_id, reaction_groups: reactionGroups ? JSON.parse(reactionGroups) : {}, updated_at: updatedAt, user: mapStorableToUser(user), diff --git a/package/src/store/schema.ts b/package/src/store/schema.ts index 1f657095f5..995b423510 100644 --- a/package/src/store/schema.ts +++ b/package/src/store/schema.ts @@ -103,6 +103,8 @@ export const tables: Tables = { extraData: 'TEXT', id: 'TEXT', messageTextUpdatedAt: 'TEXT', + poll: 'TEXT', + poll_id: 'TEXT', reactionGroups: 'TEXT', text: "TEXT DEFAULT ''", type: 'TEXT', @@ -264,6 +266,8 @@ export type Schema = { extraData: string; id: string; messageTextUpdatedAt: string; + poll: string; + poll_id: string; reactionGroups: string; type: MessageLabel; updatedAt: string; diff --git a/package/yarn.lock b/package/yarn.lock index 3d9121a887..95cfd70813 100644 --- a/package/yarn.lock +++ b/package/yarn.lock @@ -10664,10 +10664,10 @@ stream-browserify@^2.0.1: inherits "~2.0.1" readable-stream "^2.0.2" -stream-chat@8.41.1: - version "8.41.1" - resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.41.1.tgz#c991980b800b67ec38202a1aa3bbbd4112ccb5fa" - integrity sha512-WV0mHHm88D4RbAV42sD0+SqTWLCvjIwfGZ3nSBXRAuGpVYJEqnNUhEd4OIQ+YrXVbjY7qWz9L5XRk5fZIfE9kg== +stream-chat@8.42.0: + version "8.42.0" + resolved "https://registry.yarnpkg.com/stream-chat/-/stream-chat-8.42.0.tgz#124ea2c10c6e8f7990304e1101c66751daf63e6c" + integrity sha512-8xZz+fmdHSOa3L1rHUOC4Wah+ipvLvdiOmeOfGK6uXnLOKlSHMOblwmQErrOoFM4SKfX9Bea3V8viaKUu6bPng== dependencies: "@babel/runtime" "^7.16.3" "@types/jsonwebtoken" "~9.0.0"