Edit message: Don't allow send unless message contents changed
This commit is contained in:
		
					parent
					
						
							
								5987350dbe
							
						
					
				
			
			
				commit
				
					
						f53e956810
					
				
			
		
					 5 changed files with 70 additions and 8 deletions
				
			
		|  | @ -82,6 +82,9 @@ | ||||||
|       &::before { |       &::before { | ||||||
|         @include color-svg('../images/icons/v3/check/check.svg', $color-white); |         @include color-svg('../images/icons/v3/check/check.svg', $color-white); | ||||||
|       } |       } | ||||||
|  |       &:disabled { | ||||||
|  |         opacity: 0.5; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -359,7 +359,15 @@ export const CompositionArea = memo(function CompositionArea({ | ||||||
|   const editedMessageId = draftEditMessage?.targetMessageId; |   const editedMessageId = draftEditMessage?.targetMessageId; | ||||||
| 
 | 
 | ||||||
|   const handleSubmit = useCallback( |   const handleSubmit = useCallback( | ||||||
|     (message: string, bodyRanges: DraftBodyRanges, timestamp: number) => { |     ( | ||||||
|  |       message: string, | ||||||
|  |       bodyRanges: DraftBodyRanges, | ||||||
|  |       timestamp: number | ||||||
|  |     ): boolean => { | ||||||
|  |       if (!dirty) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       emojiButtonRef.current?.close(); |       emojiButtonRef.current?.close(); | ||||||
| 
 | 
 | ||||||
|       if (editedMessageId) { |       if (editedMessageId) { | ||||||
|  | @ -380,9 +388,12 @@ export const CompositionArea = memo(function CompositionArea({ | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|       setLarge(false); |       setLarge(false); | ||||||
|  | 
 | ||||||
|  |       return true; | ||||||
|     }, |     }, | ||||||
|     [ |     [ | ||||||
|       conversationId, |       conversationId, | ||||||
|  |       dirty, | ||||||
|       draftAttachments, |       draftAttachments, | ||||||
|       editedMessageId, |       editedMessageId, | ||||||
|       quotedMessageSentAt, |       quotedMessageSentAt, | ||||||
|  | @ -592,6 +603,7 @@ export const CompositionArea = memo(function CompositionArea({ | ||||||
|         <button |         <button | ||||||
|           aria-label={i18n('icu:CompositionArea__edit-action--send')} |           aria-label={i18n('icu:CompositionArea__edit-action--send')} | ||||||
|           className="CompositionArea__edit-button CompositionArea__edit-button--accept" |           className="CompositionArea__edit-button CompositionArea__edit-button--accept" | ||||||
|  |           disabled={!dirty} | ||||||
|           onClick={() => inputApiRef.current?.submit()} |           onClick={() => inputApiRef.current?.submit()} | ||||||
|           type="button" |           type="button" | ||||||
|         /> |         /> | ||||||
|  |  | ||||||
|  | @ -22,7 +22,12 @@ import type { | ||||||
|   HydratedBodyRangesType, |   HydratedBodyRangesType, | ||||||
|   RangeNode, |   RangeNode, | ||||||
| } from '../types/BodyRange'; | } from '../types/BodyRange'; | ||||||
| import { BodyRange, collapseRangeTree, insertRange } from '../types/BodyRange'; | import { | ||||||
|  |   BodyRange, | ||||||
|  |   areBodyRangesEqual, | ||||||
|  |   collapseRangeTree, | ||||||
|  |   insertRange, | ||||||
|  | } from '../types/BodyRange'; | ||||||
| import type { LocalizerType, ThemeType } from '../types/Util'; | import type { LocalizerType, ThemeType } from '../types/Util'; | ||||||
| import type { ConversationType } from '../state/ducks/conversations'; | import type { ConversationType } from '../state/ducks/conversations'; | ||||||
| import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; | import type { PreferredBadgeSelectorType } from '../state/selectors/badges'; | ||||||
|  | @ -359,7 +364,11 @@ export function CompositionInput(props: Props): React.ReactElement { | ||||||
|       `CompositionInput: Submitting message ${timestamp} with ${bodyRanges.length} ranges` |       `CompositionInput: Submitting message ${timestamp} with ${bodyRanges.length} ranges` | ||||||
|     ); |     ); | ||||||
|     canSendRef.current = false; |     canSendRef.current = false; | ||||||
|     onSubmit(text, bodyRanges, timestamp); |     const didSend = onSubmit(text, bodyRanges, timestamp); | ||||||
|  | 
 | ||||||
|  |     if (!didSend) { | ||||||
|  |       canSendRef.current = true; | ||||||
|  |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|   if (inputApi) { |   if (inputApi) { | ||||||
|  | @ -579,7 +588,19 @@ export function CompositionInput(props: Props): React.ReactElement { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (propsRef.current.onDirtyChange) { |     if (propsRef.current.onDirtyChange) { | ||||||
|       propsRef.current.onDirtyChange(text.length > 0); |       let isDirty: boolean = false; | ||||||
|  | 
 | ||||||
|  |       if (!draftEditMessage) { | ||||||
|  |         isDirty = text.length > 0; | ||||||
|  |       } else if (text.trimEnd() !== draftEditMessage.body.trimEnd()) { | ||||||
|  |         isDirty = true; | ||||||
|  |       } else if (bodyRanges.length !== draftEditMessage.bodyRanges?.length) { | ||||||
|  |         isDirty = true; | ||||||
|  |       } else if (!areBodyRangesEqual(bodyRanges, draftEditMessage.bodyRanges)) { | ||||||
|  |         isDirty = true; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       propsRef.current.onDirtyChange(isDirty); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1913,18 +1913,20 @@ function setMessageToEdit( | ||||||
|         : undefined; |         : undefined; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     const draftBodyRanges = processBodyRanges(message, { | ||||||
|  |       conversationSelector: getConversationSelector(getState()), | ||||||
|  |     }); | ||||||
|     conversation.set({ |     conversation.set({ | ||||||
|       draftEditMessage: { |       draftEditMessage: { | ||||||
|         body: message.body, |         body: message.body, | ||||||
|  |         bodyRanges: draftBodyRanges, | ||||||
|         editHistoryLength: message.editHistory?.length ?? 0, |         editHistoryLength: message.editHistory?.length ?? 0, | ||||||
|         attachmentThumbnail, |         attachmentThumbnail, | ||||||
|         preview: message.preview ? message.preview[0] : undefined, |         preview: message.preview ? message.preview[0] : undefined, | ||||||
|         targetMessageId: messageId, |         targetMessageId: messageId, | ||||||
|         quote: message.quote, |         quote: message.quote, | ||||||
|       }, |       }, | ||||||
|       draftBodyRanges: processBodyRanges(message, { |       draftBodyRanges, | ||||||
|         conversationSelector: getConversationSelector(getState()), |  | ||||||
|       }), |  | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     dispatch({ |     dispatch({ | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ | ||||||
| 
 | 
 | ||||||
| /* eslint-disable @typescript-eslint/no-namespace */ | /* eslint-disable @typescript-eslint/no-namespace */ | ||||||
| 
 | 
 | ||||||
| import { isNumber, omit, partition } from 'lodash'; | import { isEqual, isNumber, omit, orderBy, partition } from 'lodash'; | ||||||
| 
 | 
 | ||||||
| import { SignalService as Proto } from '../protobuf'; | import { SignalService as Proto } from '../protobuf'; | ||||||
| import * as log from '../logging/log'; | import * as log from '../logging/log'; | ||||||
|  | @ -849,3 +849,27 @@ export function applyRangesToText( | ||||||
| 
 | 
 | ||||||
|   return state; |   return state; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // For ease of working with draft mentions in Quill, a conversationID field is present.
 | ||||||
|  | function normalizeBodyRanges(bodyRanges: DraftBodyRanges) { | ||||||
|  |   return orderBy(bodyRanges, ['start', 'length']).map(item => { | ||||||
|  |     if (BodyRange.isMention(item)) { | ||||||
|  |       return { ...item, conversationID: undefined }; | ||||||
|  |     } | ||||||
|  |     return item; | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export function areBodyRangesEqual( | ||||||
|  |   left: DraftBodyRanges, | ||||||
|  |   right: DraftBodyRanges | ||||||
|  | ): boolean { | ||||||
|  |   const normalizedLeft = normalizeBodyRanges(left); | ||||||
|  |   const sortedRight = normalizeBodyRanges(right); | ||||||
|  | 
 | ||||||
|  |   if (normalizedLeft.length !== sortedRight.length) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return isEqual(normalizedLeft, sortedRight); | ||||||
|  | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Scott Nonnenberg
				Scott Nonnenberg