Просмотр исходного кода

feat(polls-history): control polls through local storage

factor2
Calin-Teodor 1 год назад
Родитель
Сommit
60b4581cb5

+ 1
- 0
lang/main.json Просмотреть файл

@@ -877,6 +877,7 @@
877 877
             "submit": "Submit"
878 878
         },
879 879
         "by": "By {{ name }}",
880
+        "closeButton": "Close poll",
880 881
         "create": {
881 882
             "addOption": "Add option",
882 883
             "answerPlaceholder": "Option {{index}}",

+ 1
- 0
react/features/app/middlewares.any.ts Просмотреть файл

@@ -39,6 +39,7 @@ import '../notifications/middleware';
39 39
 import '../overlay/middleware';
40 40
 import '../participants-pane/middleware';
41 41
 import '../polls/middleware';
42
+import '../polls-history/middleware';
42 43
 import '../reactions/middleware';
43 44
 import '../recent-list/middleware';
44 45
 import '../recording/middleware';

+ 1
- 0
react/features/app/reducers.any.ts Просмотреть файл

@@ -41,6 +41,7 @@ import '../notifications/reducer';
41 41
 import '../overlay/reducer';
42 42
 import '../participants-pane/reducer';
43 43
 import '../polls/reducer';
44
+import '../polls-history/reducer';
44 45
 import '../reactions/reducer';
45 46
 import '../recent-list/reducer';
46 47
 import '../recording/reducer';

+ 2
- 0
react/features/app/types.ts Просмотреть файл

@@ -60,6 +60,7 @@ import { INotificationsState } from '../notifications/reducer';
60 60
 import { IOverlayState } from '../overlay/reducer';
61 61
 import { IParticipantsPaneState } from '../participants-pane/reducer';
62 62
 import { IPollsState } from '../polls/reducer';
63
+import { IPollsHistoryState } from '../polls-history/reducer';
63 64
 import { IPowerMonitorState } from '../power-monitor/reducer';
64 65
 import { IPrejoinState } from '../prejoin/reducer';
65 66
 import { IReactionsState } from '../reactions/reducer';
@@ -149,6 +150,7 @@ export interface IReduxState {
149 150
     'features/overlay': IOverlayState;
150 151
     'features/participants-pane': IParticipantsPaneState;
151 152
     'features/polls': IPollsState;
153
+    'features/polls-history': IPollsHistoryState;
152 154
     'features/power-monitor': IPowerMonitorState;
153 155
     'features/prejoin': IPrejoinState;
154 156
     'features/reactions': IReactionsState;

+ 0
- 8
react/features/base/conference/functions.ts Просмотреть файл

@@ -37,14 +37,6 @@ import { IJitsiConference } from './reducer';
37 37
  */
38 38
 export const getConferenceState = (state: IReduxState) => state['features/base/conference'];
39 39
 
40
-/**
41
- * Is the conference joined or not.
42
- *
43
- * @param {IReduxState} state - Global state.
44
- * @returns {boolean}
45
- */
46
-export const getIsConferenceJoined = (state: IReduxState) => Boolean(getConferenceState(state).conference);
47
-
48 40
 /**
49 41
  * Attach a set of local tracks to a conference.
50 42
  *

+ 23
- 0
react/features/polls-history/actionTypes.ts Просмотреть файл

@@ -0,0 +1,23 @@
1
+/**
2
+ * The type of the action which signals that we need to remove poll from the history(local storage).
3
+ *
4
+ * {
5
+ *     type: REMOVE_POLL_FROM_HISTORY,
6
+ *     meetingId: string,
7
+ *     pollId: string,
8
+ *     poll: IPoll
9
+ * }
10
+ */
11
+export const REMOVE_POLL_FROM_HISTORY = 'REMOVE_POLL_FROM_HISTORY';
12
+
13
+/**
14
+ * The type of the action triggered when the poll is saved in history(local storage).
15
+ *
16
+ * {
17
+ *     type: SAVE_POLL_IN_HISTORY,
18
+ *     poll: Poll,
19
+ *     pollId: string,
20
+ *     saved: boolean
21
+ * }
22
+ */
23
+export const SAVE_POLL_IN_HISTORY = 'SAVE_POLL_IN_HISTORY';

+ 47
- 0
react/features/polls-history/actions.ts Просмотреть файл

@@ -0,0 +1,47 @@
1
+import { IPoll } from '../polls/types';
2
+
3
+import { REMOVE_POLL_FROM_HISTORY, SAVE_POLL_IN_HISTORY } from './actionTypes';
4
+
5
+/**
6
+ * Action to signal saving a poll in history(local storage).
7
+ *
8
+ * @param {string} meetingId - The id of the meeting in which polls get to be saved.
9
+ * @param {string} pollId - The id of the poll that gets to be saved.
10
+ * @param {IPoll} poll - The Poll object that gets to be saved.
11
+ * @returns {{
12
+ *     type: SAVE_POLL_IN_HISTORY,
13
+ *     meetingId: string,
14
+ *     pollId: string,
15
+ *     poll: IPoll
16
+ * }}
17
+ */
18
+export function savePollInHistory(meetingId: string | undefined, pollId: string, poll: IPoll) {
19
+    return {
20
+        type: SAVE_POLL_IN_HISTORY,
21
+        meetingId,
22
+        pollId,
23
+        poll
24
+    };
25
+}
26
+
27
+/**
28
+ * Action to signal that existing poll needs to be deleted from history(local storage).
29
+ *
30
+ * @param {string} meetingId - The id of the meeting in which poll gets to be removed.
31
+ * @param {string} pollId - The id of the poll that gets to be removed.
32
+ * @param {IPoll} poll - The incoming IPoll object.
33
+ * @returns {{
34
+ *     type: REMOVE_POLL_FROM_HISTORY,
35
+ *     meetingId: string,
36
+ *     pollId: string,
37
+ *     poll: IPoll
38
+ * }}
39
+ */
40
+export const removePollFromHistory = (meetingId: string | undefined, pollId: string, poll: IPoll) => {
41
+    return {
42
+        type: REMOVE_POLL_FROM_HISTORY,
43
+        meetingId,
44
+        pollId,
45
+        poll
46
+    };
47
+};

+ 46
- 0
react/features/polls-history/middleware.ts Просмотреть файл

@@ -0,0 +1,46 @@
1
+import { CONFERENCE_JOINED } from '../base/conference/actionTypes';
2
+import MiddlewareRegistry from '../base/redux/MiddlewareRegistry';
3
+import { REMOVE_POLL, SAVE_POLL } from '../polls/actionTypes';
4
+import { savePoll } from '../polls/actions';
5
+
6
+import { removePollFromHistory, savePollInHistory } from './actions';
7
+
8
+MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
9
+    const result = next(action);
10
+    const { room: meetingId } = getState()['features/base/conference'];
11
+
12
+    switch (action.type) {
13
+
14
+    case CONFERENCE_JOINED: {
15
+        const state = getState();
16
+        const pollsHistory = meetingId && state['features/polls-history'].polls?.[meetingId];
17
+
18
+        if (!pollsHistory) {
19
+            return null;
20
+        }
21
+
22
+        for (const key in pollsHistory) {
23
+            if (pollsHistory.hasOwnProperty(key) && pollsHistory[key].saved) {
24
+                dispatch(savePoll(key, pollsHistory[key]));
25
+            }
26
+        }
27
+        break;
28
+    }
29
+
30
+    case REMOVE_POLL: {
31
+        const { poll, pollId } = action;
32
+
33
+        dispatch(removePollFromHistory(meetingId, pollId, poll));
34
+        break;
35
+    }
36
+
37
+    case SAVE_POLL: {
38
+        const { poll, pollId } = action;
39
+
40
+        dispatch(savePollInHistory(meetingId, pollId, poll));
41
+        break;
42
+    }
43
+    }
44
+
45
+    return result;
46
+});

+ 52
- 0
react/features/polls-history/reducer.ts Просмотреть файл

@@ -0,0 +1,52 @@
1
+import PersistenceRegistry from '../base/redux/PersistenceRegistry';
2
+import ReducerRegistry from '../base/redux/ReducerRegistry';
3
+import { IPoll } from '../polls/types';
4
+
5
+import { REMOVE_POLL_FROM_HISTORY, SAVE_POLL_IN_HISTORY } from './actionTypes';
6
+
7
+const INITIAL_STATE = {
8
+    polls: {}
9
+};
10
+
11
+export interface IPollsHistoryState {
12
+    polls: {
13
+        [meetingId: string]: {
14
+            [pollId: string]: IPoll;
15
+        };
16
+    };
17
+}
18
+
19
+const STORE_NAME = 'features/polls-history';
20
+
21
+PersistenceRegistry.register(STORE_NAME, INITIAL_STATE);
22
+
23
+ReducerRegistry.register<IPollsHistoryState>(STORE_NAME, (state = INITIAL_STATE, action): IPollsHistoryState => {
24
+    switch (action.type) {
25
+
26
+    case REMOVE_POLL_FROM_HISTORY: {
27
+        if (Object.keys(state.polls[action.meetingId] ?? {})?.length === 1) {
28
+            delete state.polls[action.meetingId];
29
+        } else {
30
+            delete state.polls[action.meetingId]?.[action.pollId];
31
+        }
32
+
33
+        return state;
34
+    }
35
+
36
+    case SAVE_POLL_IN_HISTORY: {
37
+        return {
38
+            ...state,
39
+            polls: {
40
+                ...state.polls,
41
+                [action.meetingId]: {
42
+                    ...state.polls[action.meetingId],
43
+                    [action.pollId]: action.poll
44
+                }
45
+            }
46
+        };
47
+    }
48
+
49
+    default:
50
+        return state;
51
+    }
52
+});

+ 5
- 4
react/features/polls/actionTypes.ts Просмотреть файл

@@ -10,7 +10,7 @@ export const CHANGE_VOTE = 'CHANGE_VOTE';
10 10
 
11 11
 /**
12 12
  * The type of the action which signals that we need to clear all polls from the state.
13
- * For example we are moving to another conference.
13
+ * For example, we are moving to another conference.
14 14
  *
15 15
  * {
16 16
  *     type: CLEAR_POLLS
@@ -65,14 +65,15 @@ export const RECEIVE_ANSWER = 'RECEIVE_ANSWER';
65 65
 export const REGISTER_VOTE = 'REGISTER_VOTE';
66 66
 
67 67
 /**
68
- * The type of the action which retracts a vote.
68
+ * The type of the action which signals that we need to remove poll.
69 69
  *
70 70
  * {
71
- *     type: RETRACT_VOTE,
71
+ *     type: REMOVE_POLL,
72 72
  *     pollId: string,
73
+ *     poll: IPoll
73 74
  * }
74 75
  */
75
-export const RETRACT_VOTE = 'RETRACT_VOTE';
76
+export const REMOVE_POLL = 'REMOVE_POLL';
76 77
 
77 78
 /**
78 79
  * The type of the action triggered when the poll tab in chat pane is closed

+ 41
- 40
react/features/polls/actions.ts Просмотреть файл

@@ -5,8 +5,8 @@ import {
5 5
     RECEIVE_ANSWER,
6 6
     RECEIVE_POLL,
7 7
     REGISTER_VOTE,
8
+    REMOVE_POLL,
8 9
     RESET_NB_UNREAD_POLLS,
9
-    RETRACT_VOTE,
10 10
     SAVE_POLL
11 11
 } from './actionTypes';
12 12
 import { IAnswer, IPoll } from './types';
@@ -19,7 +19,9 @@ import { IAnswer, IPoll } from './types';
19 19
  * }}
20 20
  */
21 21
 export const clearPolls = () => {
22
-    return { type: CLEAR_POLLS };
22
+    return {
23
+        type: CLEAR_POLLS
24
+    };
23 25
 };
24 26
 
25 27
 /**
@@ -50,16 +52,16 @@ export const setVoteChanging = (pollId: string, value: boolean) => {
50 52
  * @param {boolean} notify - Whether to send or not a notification.
51 53
  * @returns {{
52 54
  *     type: RECEIVE_POLL,
53
- *     poll: IPoll,
54 55
  *     pollId: string,
56
+ *     poll: IPoll,
55 57
  *     notify: boolean
56 58
  * }}
57 59
  */
58 60
 export const receivePoll = (pollId: string, poll: IPoll, notify: boolean) => {
59 61
     return {
60 62
         type: RECEIVE_POLL,
61
-        poll,
62 63
         pollId,
64
+        poll,
63 65
         notify
64 66
     };
65 67
 };
@@ -71,15 +73,15 @@ export const receivePoll = (pollId: string, poll: IPoll, notify: boolean) => {
71 73
  * @param {IAnswer} answer - The incoming Answer object.
72 74
  * @returns {{
73 75
  *     type: RECEIVE_ANSWER,
74
- *     answer: IAnswer,
75
- *     pollId: string
76
+ *     pollId: string,
77
+ *     answer: IAnswer
76 78
  * }}
77 79
  */
78 80
 export const receiveAnswer = (pollId: string, answer: IAnswer) => {
79 81
     return {
80 82
         type: RECEIVE_ANSWER,
81
-        answer,
82
-        pollId
83
+        pollId,
84
+        answer
83 85
     };
84 86
 };
85 87
 
@@ -90,39 +92,23 @@ export const receiveAnswer = (pollId: string, answer: IAnswer) => {
90 92
  * @param {?Array<boolean>} answers - The new answers.
91 93
  * @returns {{
92 94
  *     type: REGISTER_VOTE,
93
- *     answers: ?Array<boolean>,
94
- *     pollId: string
95
+ *     pollId: string,
96
+ *     answers: ?Array<boolean>
95 97
  * }}
96 98
  */
97 99
 export const registerVote = (pollId: string, answers: Array<boolean> | null) => {
98 100
     return {
99 101
         type: REGISTER_VOTE,
100
-        answers,
101
-        pollId
102
-    };
103
-};
104
-
105
-/**
106
- * Action to retract a vote on a poll.
107
- *
108
- * @param {string} pollId - The id of the poll.
109
- * @returns {{
110
- *     type: RETRACT_VOTE,
111
- *     pollId: string
112
- * }}
113
- */
114
-export const retractVote = (pollId: string) => {
115
-    return {
116
-        type: RETRACT_VOTE,
117
-        pollId
102
+        pollId,
103
+        answers
118 104
     };
119 105
 };
120 106
 
121 107
 /**
122
- * Action to signal the closing of the polls tab.
108
+ * Action to signal the number reset of unread polls.
123 109
  *
124 110
  * @returns {{
125
- *     type: POLL_TAB_CLOSED
111
+ *     type: RESET_NB_UNREAD_POLLS
126 112
  * }}
127 113
  */
128 114
 export function resetNbUnreadPollsMessages() {
@@ -136,22 +122,18 @@ export function resetNbUnreadPollsMessages() {
136 122
  *
137 123
  * @param {string} pollId - The id of the poll that gets to be saved.
138 124
  * @param {IPoll} poll - The Poll object that gets to be saved.
139
- * @param {boolean} saved - Whether the poll is saved or not.
140 125
  * @returns {{
141
- *     type: RECEIVE_POLL,
142
- *     poll: IPoll,
126
+ *     type: SAVE_POLL,
127
+ *     meetingId: string,
143 128
  *     pollId: string,
144
- *     saved: boolean
129
+ *     poll: IPoll
145 130
  * }}
146 131
  */
147
-export function savePoll(pollId: string, poll: IPoll, saved: boolean) {
132
+export function savePoll(pollId: string, poll: IPoll) {
148 133
     return {
149 134
         type: SAVE_POLL,
150 135
         pollId,
151
-        poll: {
152
-            ...poll,
153
-            saved
154
-        }
136
+        poll
155 137
     };
156 138
 }
157 139
 
@@ -161,7 +143,7 @@ export function savePoll(pollId: string, poll: IPoll, saved: boolean) {
161 143
  * @param {string} pollId - The id of the poll that gets to be edited.
162 144
  * @param {boolean} editing - Whether the poll is in edit mode or not.
163 145
  * @returns {{
164
- *     type: RECEIVE_POLL,
146
+ *     type: EDIT_POLL,
165 147
  *     pollId: string,
166 148
  *     editing: boolean
167 149
  * }}
@@ -173,3 +155,22 @@ export function editPoll(pollId: string, editing: boolean) {
173 155
         editing
174 156
     };
175 157
 }
158
+
159
+/**
160
+ * Action to signal that existing polls needs to be removed.
161
+ *
162
+ * @param {string} pollId - The id of the poll that gets to be removed.
163
+ * @param {IPoll} poll - The incoming Poll object.
164
+ * @returns {{
165
+ *     type: REMOVE_POLL,
166
+ *     pollId: string,
167
+ *     poll: IPoll
168
+ * }}
169
+ */
170
+export const removePoll = (pollId: string, poll: IPoll) => {
171
+    return {
172
+        type: REMOVE_POLL,
173
+        pollId,
174
+        poll
175
+    };
176
+};

+ 6
- 6
react/features/polls/components/AbstractPollAnswer.tsx Просмотреть файл

@@ -7,8 +7,9 @@ import { sendAnalytics } from '../../analytics/functions';
7 7
 import { IReduxState } from '../../app/types';
8 8
 import { getParticipantDisplayName } from '../../base/participants/functions';
9 9
 import { useBoundSelector } from '../../base/util/hooks';
10
-import { editPoll, registerVote, setVoteChanging } from '../actions';
10
+import { registerVote, removePoll, setVoteChanging } from '../actions';
11 11
 import { COMMAND_ANSWER_POLL, COMMAND_NEW_POLL } from '../constants';
12
+import { getPoll } from '../functions';
12 13
 import { IPoll } from '../types';
13 14
 
14 15
 /**
@@ -48,9 +49,9 @@ const AbstractPollAnswer = (Component: ComponentType<AbstractProps>) => (props:
48 49
 
49 50
     const { pollId, setCreateMode } = props;
50 51
 
51
-    const conference: any = useSelector((state: IReduxState) => state['features/base/conference'].conference);
52
+    const { conference } = useSelector((state: IReduxState) => state['features/base/conference']);
52 53
 
53
-    const poll: IPoll = useSelector((state: IReduxState) => state['features/polls'].polls[pollId]);
54
+    const poll: IPoll = useSelector(getPoll(pollId));
54 55
 
55 56
     const { answers, lastVote, question, senderId } = poll;
56 57
 
@@ -75,7 +76,7 @@ const AbstractPollAnswer = (Component: ComponentType<AbstractProps>) => (props:
75 76
     const dispatch = useDispatch();
76 77
 
77 78
     const submitAnswer = useCallback(() => {
78
-        conference.sendMessage({
79
+        conference?.sendMessage({
79 80
             type: COMMAND_ANSWER_POLL,
80 81
             pollId,
81 82
             answers: checkBoxStates
@@ -95,8 +96,7 @@ const AbstractPollAnswer = (Component: ComponentType<AbstractProps>) => (props:
95 96
             answers: answers.map(answer => answer.name)
96 97
         });
97 98
 
98
-        dispatch(editPoll(pollId, false));
99
-
99
+        dispatch(removePoll(pollId, poll));
100 100
     }, [ conference, question, answers ]);
101 101
 
102 102
     const skipAnswer = useCallback(() => {

+ 4
- 4
react/features/polls/components/AbstractPollCreate.tsx Просмотреть файл

@@ -121,7 +121,7 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
121 121
         setAnswers(newAnswers);
122 122
     }, [ answers ]);
123 123
 
124
-    const conference = useSelector((state: IReduxState) => state['features/base/conference'].conference);
124
+    const { conference } = useSelector((state: IReduxState) => state['features/base/conference']);
125 125
 
126 126
     const dispatch = useDispatch();
127 127
 
@@ -147,14 +147,14 @@ const AbstractPollCreate = (Component: ComponentType<AbstractProps>) => (props:
147 147
             lastVote: null,
148 148
             question,
149 149
             answers: filteredAnswers,
150
-            saved: false,
150
+            saved: true,
151 151
             editing: false
152 152
         };
153 153
 
154 154
         if (editingPoll) {
155
-            dispatch(savePoll(editingPoll[0], poll, true));
155
+            dispatch(savePoll(editingPoll[0], poll));
156 156
         } else {
157
-            dispatch(savePoll(pollId, poll, true));
157
+            dispatch(savePoll(pollId, poll));
158 158
         }
159 159
 
160 160
         sendAnalytics(createPollEvent('created'));

+ 8
- 7
react/features/polls/components/AbstractPollResults.tsx Просмотреть файл

@@ -10,6 +10,7 @@ import { getParticipantById, getParticipantDisplayName } from '../../base/partic
10 10
 import { useBoundSelector } from '../../base/util/hooks';
11 11
 import { setVoteChanging } from '../actions';
12 12
 import { getPoll } from '../functions';
13
+import { IPoll } from '../types';
13 14
 
14 15
 /**
15 16
  * The type of the React {@code Component} props of inheriting component.
@@ -53,8 +54,8 @@ export type AbstractProps = {
53 54
 const AbstractPollResults = (Component: ComponentType<AbstractProps>) => (props: InputProps) => {
54 55
     const { pollId } = props;
55 56
 
56
-    const pollDetails = useSelector(getPoll(pollId));
57
-    const participant = useBoundSelector(getParticipantById, pollDetails.senderId);
57
+    const poll: IPoll = useSelector(getPoll(pollId));
58
+    const participant = useBoundSelector(getParticipantById, poll.senderId);
58 59
     const reduxState = useSelector((state: IReduxState) => state);
59 60
 
60 61
     const [ showDetails, setShowDetails ] = useState(false);
@@ -67,14 +68,14 @@ const AbstractPollResults = (Component: ComponentType<AbstractProps>) => (props:
67 68
         const allVoters = new Set();
68 69
 
69 70
         // Getting every voters ID that participates to the poll
70
-        for (const answer of pollDetails.answers) {
71
+        for (const answer of poll.answers) {
71 72
             // checking if the voters is an array for supporting old structure model
72 73
             const voters: string[] = answer.voters.length ? answer.voters : Object.keys(answer.voters);
73 74
 
74 75
             voters.forEach((voter: string) => allVoters.add(voter));
75 76
         }
76 77
 
77
-        return pollDetails.answers.map(answer => {
78
+        return poll.answers.map(answer => {
78 79
             const nrOfVotersPerAnswer = answer.voters ? Object.keys(answer.voters).length : 0;
79 80
             const percentage = allVoters.size > 0 ? Math.round(nrOfVotersPerAnswer / allVoters.size * 100) : 0;
80 81
 
@@ -98,7 +99,7 @@ const AbstractPollResults = (Component: ComponentType<AbstractProps>) => (props:
98 99
                 voterCount: nrOfVotersPerAnswer
99 100
             };
100 101
         });
101
-    }, [ pollDetails.answers, showDetails ]);
102
+    }, [ poll.answers, showDetails ]);
102 103
 
103 104
     const dispatch = useDispatch();
104 105
     const changeVote = useCallback(() => {
@@ -113,8 +114,8 @@ const AbstractPollResults = (Component: ComponentType<AbstractProps>) => (props:
113 114
             answers = { answers }
114 115
             changeVote = { changeVote }
115 116
             creatorName = { participant ? participant.name : '' }
116
-            haveVoted = { pollDetails.lastVote !== null }
117
-            question = { pollDetails.question }
117
+            haveVoted = { poll.lastVote !== null }
118
+            question = { poll.question }
118 119
             showDetails = { showDetails }
119 120
             t = { t }
120 121
             toggleIsDetailed = { toggleIsDetailed } />

+ 17
- 6
react/features/polls/components/native/PollAnswer.tsx Просмотреть файл

@@ -4,11 +4,13 @@ import React from 'react';
4 4
 import { Text, TextStyle, View, ViewStyle } from 'react-native';
5 5
 import { useDispatch, useSelector } from 'react-redux';
6 6
 
7
+import { IconCloseLarge } from '../../../base/icons/svg';
7 8
 import { getLocalParticipant } from '../../../base/participants/functions';
8 9
 import Button from '../../../base/ui/components/native/Button';
10
+import IconButton from '../../../base/ui/components/native/IconButton';
9 11
 import Switch from '../../../base/ui/components/native/Switch';
10 12
 import { BUTTON_TYPES } from '../../../base/ui/constants.native';
11
-import { editPoll } from '../../actions';
13
+import { editPoll, removePoll } from '../../actions';
12 14
 import { isSubmitAnswerDisabled } from '../../functions';
13 15
 import AbstractPollAnswer, { AbstractProps } from '../AbstractPollAnswer';
14 16
 
@@ -34,11 +36,20 @@ const PollAnswer = (props: AbstractProps) => {
34 36
 
35 37
     return (
36 38
         <>
37
-            <Text style = { dialogStyles.questionText as TextStyle } >{ poll.question }</Text>
38
-            <Text style = { dialogStyles.questionOwnerText as TextStyle } >{
39
-                t('polls.by', { name: localParticipant?.name })
40
-            }
41
-            </Text>
39
+            <View style = { dialogStyles.headerContainer as ViewStyle }>
40
+                <View>
41
+                    <Text style = { dialogStyles.questionText as TextStyle } >{ poll.question }</Text>
42
+                    <Text style = { dialogStyles.questionOwnerText as TextStyle } >{
43
+                        t('polls.by', { name: localParticipant?.name })
44
+                    }
45
+                    </Text>
46
+                </View>
47
+                {
48
+                    pollSaved && <IconButton
49
+                        onPress = { () => dispatch(removePoll(pollId, poll)) }
50
+                        src = { IconCloseLarge } />
51
+                }
52
+            </View>
42 53
             <View style = { pollsStyles.answerContent as ViewStyle }>
43 54
                 {
44 55
                     poll.answers.map((answer, index: number) => (

+ 6
- 0
react/features/polls/components/native/styles.ts Просмотреть файл

@@ -4,6 +4,12 @@ import BaseTheme from '../../../base/ui/components/BaseTheme.native';
4 4
 
5 5
 export const dialogStyles = createStyleSheet({
6 6
 
7
+    headerContainer: {
8
+        display: 'flex',
9
+        flexDirection: 'row',
10
+        justifyContent: 'space-between'
11
+    },
12
+
7 13
     customContainer: {
8 14
         marginBottom: BaseTheme.spacing[3],
9 15
         marginHorizontal: BaseTheme.spacing[3],

+ 16
- 1
react/features/polls/components/web/PollAnswer.tsx Просмотреть файл

@@ -4,11 +4,13 @@ import React from 'react';
4 4
 import { useDispatch } from 'react-redux';
5 5
 import { makeStyles } from 'tss-react/mui';
6 6
 
7
+import Icon from '../../../base/icons/components/Icon';
8
+import { IconCloseLarge } from '../../../base/icons/svg';
7 9
 import { withPixelLineHeight } from '../../../base/styles/functions.web';
8 10
 import Button from '../../../base/ui/components/web/Button';
9 11
 import Checkbox from '../../../base/ui/components/web/Checkbox';
10 12
 import { BUTTON_TYPES } from '../../../base/ui/constants.web';
11
-import { editPoll } from '../../actions';
13
+import { editPoll, removePoll } from '../../actions';
12 14
 import { isSubmitAnswerDisabled } from '../../functions';
13 15
 import AbstractPollAnswer, { AbstractProps } from '../AbstractPollAnswer';
14 16
 
@@ -21,6 +23,10 @@ const useStyles = makeStyles()(theme => {
21 23
             borderRadius: '8px',
22 24
             wordBreak: 'break-word'
23 25
         },
26
+        closeBtn: {
27
+            cursor: 'pointer',
28
+            float: 'right'
29
+        },
24 30
         header: {
25 31
             marginBottom: '24px'
26 32
         },
@@ -73,6 +79,15 @@ const PollAnswer = ({
73 79
 
74 80
     return (
75 81
         <div className = { classes.container }>
82
+            {
83
+                pollSaved && <Icon
84
+                    ariaLabel = { t('polls.closeButton') }
85
+                    className = { classes.closeBtn }
86
+                    onClick = { () => dispatch(removePoll(pollId, poll)) }
87
+                    role = 'button'
88
+                    src = { IconCloseLarge }
89
+                    tabIndex = { 0 } />
90
+            }
76 91
             <div className = { classes.header }>
77 92
                 <div className = { classes.question }>
78 93
                     { poll.question }

+ 1
- 1
react/features/polls/components/web/PollsList.tsx Просмотреть файл

@@ -46,8 +46,8 @@ interface IPollListProps {
46 46
 const PollsList = ({ setCreateMode }: IPollListProps) => {
47 47
     const { t } = useTranslation();
48 48
     const { classes, theme } = useStyles();
49
+    const { polls } = useSelector((state: IReduxState) => state['features/polls']);
49 50
 
50
-    const polls = useSelector((state: IReduxState) => state['features/polls'].polls);
51 51
     const pollListEndRef = useRef<HTMLDivElement>(null);
52 52
 
53 53
     const scrollToBottom = useCallback(() => {

+ 2
- 4
react/features/polls/middleware.ts Просмотреть файл

@@ -25,10 +25,8 @@ import { IAnswer, IPoll, IPollData } from './types';
25 25
  */
26 26
 StateListenerRegistry.register(
27 27
     state => getCurrentConference(state),
28
-    (conference, { dispatch }, previousConference) => {
28
+    (conference, { dispatch }, previousConference): void => {
29 29
         if (conference !== previousConference) {
30
-            // conference changed, left or failed...
31
-            // clean old polls
32 30
             dispatch(clearPolls());
33 31
         }
34 32
     });
@@ -101,7 +99,6 @@ MiddlewareRegistry.register(({ dispatch, getState }) => next => action => {
101 99
         }
102 100
         break;
103 101
     }
104
-
105 102
     }
106 103
 
107 104
     return result;
@@ -122,6 +119,7 @@ function _handleReceivePollsMessage(data: any, dispatch: IStore['dispatch'], get
122 119
     }
123 120
 
124 121
     switch (data.type) {
122
+
125 123
     case COMMAND_NEW_POLL: {
126 124
         const { pollId, answers, senderId, question } = data;
127 125
 

+ 34
- 19
react/features/polls/reducer.ts Просмотреть файл

@@ -7,8 +7,8 @@ import {
7 7
     RECEIVE_ANSWER,
8 8
     RECEIVE_POLL,
9 9
     REGISTER_VOTE,
10
+    REMOVE_POLL,
10 11
     RESET_NB_UNREAD_POLLS,
11
-    RETRACT_VOTE,
12 12
     SAVE_POLL
13 13
 } from './actionTypes';
14 14
 import { IAnswer, IPoll } from './types';
@@ -27,7 +27,9 @@ export interface IPollsState {
27 27
     };
28 28
 }
29 29
 
30
-ReducerRegistry.register<IPollsState>('features/polls', (state = INITIAL_STATE, action): IPollsState => {
30
+const STORE_NAME = 'features/polls';
31
+
32
+ReducerRegistry.register<IPollsState>(STORE_NAME, (state = INITIAL_STATE, action): IPollsState => {
31 33
     switch (action.type) {
32 34
 
33 35
     case CHANGE_VOTE: {
@@ -54,8 +56,7 @@ ReducerRegistry.register<IPollsState>('features/polls', (state = INITIAL_STATE,
54 56
     }
55 57
 
56 58
     // Reducer triggered when a poll is received or saved.
57
-    case RECEIVE_POLL:
58
-    case SAVE_POLL: {
59
+    case RECEIVE_POLL: {
59 60
         return {
60 61
             ...state,
61 62
             polls: {
@@ -66,6 +67,16 @@ ReducerRegistry.register<IPollsState>('features/polls', (state = INITIAL_STATE,
66 67
         };
67 68
     }
68 69
 
70
+    case SAVE_POLL: {
71
+        return {
72
+            ...state,
73
+            polls: {
74
+                ...state.polls,
75
+                [action.pollId]: action.poll
76
+            }
77
+        };
78
+    }
79
+
69 80
     // Reducer triggered when an answer is received
70 81
     // The answer is added  to an existing poll
71 82
     case RECEIVE_ANSWER: {
@@ -139,21 +150,6 @@ ReducerRegistry.register<IPollsState>('features/polls', (state = INITIAL_STATE,
139 150
         };
140 151
     }
141 152
 
142
-    case RETRACT_VOTE: {
143
-        const { pollId }: { pollId: string; } = action;
144
-
145
-        return {
146
-            ...state,
147
-            polls: {
148
-                ...state.polls,
149
-                [pollId]: {
150
-                    ...state.polls[pollId],
151
-                    showResults: false
152
-                }
153
-            }
154
-        };
155
-    }
156
-
157 153
     case RESET_NB_UNREAD_POLLS: {
158 154
         return {
159 155
             ...state,
@@ -174,6 +170,25 @@ ReducerRegistry.register<IPollsState>('features/polls', (state = INITIAL_STATE,
174 170
         };
175 171
     }
176 172
 
173
+    case REMOVE_POLL: {
174
+        if (Object.keys(state.polls ?? {})?.length === 1) {
175
+            return {
176
+                ...state,
177
+                ...INITIAL_STATE
178
+            };
179
+        }
180
+
181
+        // eslint-disable-next-line @typescript-eslint/no-unused-vars
182
+        const { [action.pollId]: _removedPoll, ...newState } = state.polls;
183
+
184
+        return {
185
+            ...state,
186
+            polls: {
187
+                ...newState
188
+            }
189
+        };
190
+    }
191
+
177 192
     default:
178 193
         return state;
179 194
     }

Загрузка…
Отмена
Сохранить