Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F32088947
entry.react.js
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
15 KB
Referenced Files
None
Subscribers
None
entry.react.js
View Options
// @flow
import
classNames
from
'classnames'
;
import
invariant
from
'invariant'
;
import
*
as
React
from
'react'
;
import
{
createEntryActionTypes
,
useCreateEntry
,
saveEntryActionTypes
,
useSaveEntry
,
deleteEntryActionTypes
,
useDeleteEntry
,
concurrentModificationResetActionType
,
}
from
'lib/actions/entry-actions.js'
;
import
{
type
PushModal
,
useModalContext
,
}
from
'lib/components/modal-provider.react.js'
;
import
{
extractKeyserverIDFromID
}
from
'lib/keyserver-conn/keyserver-call-utils.js'
;
import
{
connectionSelector
}
from
'lib/selectors/keyserver-selectors.js'
;
import
{
threadInfoSelector
}
from
'lib/selectors/thread-selectors.js'
;
import
{
colorIsDark
}
from
'lib/shared/color-utils.js'
;
import
{
entryKey
}
from
'lib/shared/entry-utils.js'
;
import
{
threadHasPermission
}
from
'lib/shared/thread-utils.js'
;
import
{
type
EntryInfo
,
type
CreateEntryInfo
,
type
SaveEntryInfo
,
type
SaveEntryResult
,
type
SaveEntryPayload
,
type
CreateEntryPayload
,
type
DeleteEntryInfo
,
type
DeleteEntryResult
,
type
CalendarQuery
,
}
from
'lib/types/entry-types.js'
;
import
type
{
LoadingStatus
}
from
'lib/types/loading-types.js'
;
import
type
{
ResolvedThreadInfo
}
from
'lib/types/minimally-encoded-thread-permissions-types.js'
;
import
type
{
Dispatch
}
from
'lib/types/redux-types.js'
;
import
{
threadPermissions
}
from
'lib/types/thread-permission-types.js'
;
import
{
dateString
}
from
'lib/utils/date-utils.js'
;
import
{
useResolvedThreadInfo
}
from
'lib/utils/entity-helpers.js'
;
import
{
ServerError
}
from
'lib/utils/errors.js'
;
import
{
useDispatchActionPromise
,
type
DispatchActionPromise
,
}
from
'lib/utils/redux-promise-utils.js'
;
import
{
useDispatch
}
from
'lib/utils/redux-utils.js'
;
import
css
from
'./calendar.css'
;
import
LoadingIndicator
from
'../loading-indicator.react.js'
;
import
LogInFirstModal
from
'../modals/account/log-in-first-modal.react.js'
;
import
ConcurrentModificationModal
from
'../modals/concurrent-modification-modal.react.js'
;
import
HistoryModal
from
'../modals/history/history-modal.react.js'
;
import
{
useSelector
}
from
'../redux/redux-utils.js'
;
import
{
nonThreadCalendarQuery
}
from
'../selectors/nav-selectors.js'
;
import
{
HistoryVector
,
DeleteVector
}
from
'../vectors.react.js'
;
type
BaseProps
=
{
+
innerRef
:
(
key
:
string
,
me
:
Entry
)
=>
void
,
+
entryInfo
:
EntryInfo
,
+
focusOnFirstEntryNewerThan
:
(
time
:
number
)
=>
void
,
+
tabIndex
:
number
,
};
type
Props
=
{
...
BaseProps
,
+
threadInfo
:
ResolvedThreadInfo
,
+
loggedIn
:
boolean
,
+
calendarQuery
:
()
=>
CalendarQuery
,
+
online
:
boolean
,
+
dispatch
:
Dispatch
,
+
dispatchActionPromise
:
DispatchActionPromise
,
+
createEntry
:
(
info
:
CreateEntryInfo
)
=>
Promise
<
CreateEntryPayload
>
,
+
saveEntry
:
(
info
:
SaveEntryInfo
)
=>
Promise
<
SaveEntryResult
>
,
+
deleteEntry
:
(
info
:
DeleteEntryInfo
)
=>
Promise
<
DeleteEntryResult
>
,
+
pushModal
:
PushModal
,
+
popModal
:
()
=>
void
,
};
type
State
=
{
+
focused
:
boolean
,
+
loadingStatus
:
LoadingStatus
,
+
text
:
string
,
};
class
Entry
extends
React
.
PureComponent
<
Props
,
State
>
{
textarea
:
?
HTMLTextAreaElement
;
creating
:
boolean
;
needsUpdateAfterCreation
:
boolean
;
needsDeleteAfterCreation
:
boolean
;
nextSaveAttemptIndex
:
number
;
mounted
:
boolean
;
currentlySaving
:
?
string
;
constructor
(
props
:
Props
)
{
super
(
props
);
this
.
state
=
{
focused
:
false
,
loadingStatus
:
'inactive'
,
text
:
props
.
entryInfo
.
text
,
};
this
.
creating
=
false
;
this
.
needsUpdateAfterCreation
=
false
;
this
.
needsDeleteAfterCreation
=
false
;
this
.
nextSaveAttemptIndex
=
0
;
}
guardedSetState
(
input
:
Partial
<
State
>
)
{
if
(
this
.
mounted
)
{
this
.
setState
(
input
);
}
}
componentDidMount
()
{
this
.
mounted
=
true
;
this
.
props
.
innerRef
(
entryKey
(
this
.
props
.
entryInfo
),
this
);
this
.
updateHeight
();
// Whenever a new Entry is created, focus on it
if
(
!
this
.
props
.
entryInfo
.
id
)
{
this
.
focus
();
}
}
componentDidUpdate
(
prevProps
:
Props
)
{
if
(
!
this
.
state
.
focused
&&
this
.
props
.
entryInfo
.
text
!==
this
.
state
.
text
&&
this
.
props
.
entryInfo
.
text
!==
prevProps
.
entryInfo
.
text
)
{
this
.
setState
({
text
:
this
.
props
.
entryInfo
.
text
});
this
.
currentlySaving
=
null
;
}
if
(
this
.
props
.
online
&&
!
prevProps
.
online
&&
this
.
state
.
loadingStatus
===
'error'
)
{
this
.
save
();
}
}
focus
()
{
invariant
(
this
.
textarea
instanceof
HTMLTextAreaElement
,
'textarea ref not set'
,
);
this
.
textarea
.
focus
();
}
onMouseDown
:
(
event
:
SyntheticEvent
<
HTMLDivElement
>
)
=>
void
=
event
=>
{
if
(
this
.
state
.
focused
&&
event
.
target
!==
this
.
textarea
)
{
// Don't lose focus when some non-textarea part is clicked
event
.
preventDefault
();
}
};
componentWillUnmount
()
{
this
.
mounted
=
false
;
}
updateHeight
:
()
=>
void
=
()
=>
{
invariant
(
this
.
textarea
instanceof
HTMLTextAreaElement
,
'textarea ref not set'
,
);
this
.
textarea
.
style
.
height
=
'auto'
;
this
.
textarea
.
style
.
height
=
this
.
textarea
.
scrollHeight
+
'px'
;
};
render
()
:
React
.
Node
{
let
actionLinks
=
null
;
if
(
this
.
state
.
focused
)
{
let
historyButton
=
null
;
if
(
this
.
props
.
entryInfo
.
id
)
{
historyButton
=
(
<
a
href
=
"#"
onClick
=
{
this
.
onHistory
}
>
<
HistoryVector
className
=
{
css
.
history
}
/>
<
span
className
=
{
css
.
actionLinksText
}
>
History
<
/span>
<
/a>
);
}
const
rightActionLinksClassName
=
`
${
css
.
rightActionLinks
}
${
css
.
actionLinksText
}
`
;
actionLinks
=
(
<
div
className
=
{
css
.
actionLinks
}
>
<
a
href
=
"#"
onClick
=
{
this
.
onDelete
}
>
<
DeleteVector
/>
<
span
className
=
{
css
.
actionLinksText
}
>
Delete
<
/span>
<
/a>
{
historyButton
}
<
span
className
=
{
rightActionLinksClassName
}
>
{
this
.
props
.
threadInfo
.
uiName
}
<
/span>
<
div
className
=
{
css
.
clear
}
><
/div>
<
/div>
);
}
const
darkColor
=
colorIsDark
(
this
.
props
.
threadInfo
.
color
);
const
entryClasses
=
classNames
({
[
css
.
entry
]
:
true
,
[
css
.
darkEntry
]
:
darkColor
,
[
css
.
focusedEntry
]
:
this
.
state
.
focused
,
});
const
style
=
{
backgroundColor
:
`#
${
this
.
props
.
threadInfo
.
color
}
`
};
const
loadingIndicatorColor
=
darkColor
?
'white'
:
'black'
;
const
canEditEntry
=
threadHasPermission
(
this
.
props
.
threadInfo
,
threadPermissions
.
EDIT_ENTRIES
,
);
return
(
<
div
className
=
{
entryClasses
}
style
=
{
style
}
onMouseDown
=
{
this
.
onMouseDown
}
>
<
textarea
rows
=
"1"
className
=
{
css
.
entryText
}
onChange
=
{
this
.
onChange
}
onKeyDown
=
{
this
.
onKeyDown
}
value
=
{
this
.
state
.
text
}
onFocus
=
{
this
.
onFocus
}
onBlur
=
{
this
.
onBlur
}
tabIndex
=
{
this
.
props
.
tabIndex
}
ref
=
{
this
.
textareaRef
}
disabled
=
{
!
canEditEntry
}
/>
<
LoadingIndicator
status
=
{
this
.
state
.
loadingStatus
}
color
=
{
loadingIndicatorColor
}
loadingClassName
=
{
css
.
entryLoading
}
errorClassName
=
{
css
.
entryError
}
/>
{
actionLinks
}
<
/div>
);
}
textareaRef
:
(
textarea
:
?
HTMLTextAreaElement
)
=>
void
=
textarea
=>
{
this
.
textarea
=
textarea
;
};
onFocus
:
()
=>
void
=
()
=>
{
if
(
!
this
.
state
.
focused
)
{
this
.
setState
({
focused
:
true
});
}
};
onBlur
:
()
=>
void
=
()
=>
{
this
.
setState
({
focused
:
false
});
if
(
this
.
state
.
text
.
trim
()
===
''
)
{
this
.
delete
();
}
else
if
(
this
.
props
.
entryInfo
.
text
!==
this
.
state
.
text
)
{
this
.
save
();
}
};
delete
()
{
this
.
dispatchDelete
(
this
.
props
.
entryInfo
.
id
,
false
);
}
save
()
{
this
.
dispatchSave
(
this
.
props
.
entryInfo
.
id
,
this
.
state
.
text
);
}
onChange
:
(
event
:
SyntheticEvent
<
HTMLTextAreaElement
>
)
=>
void
=
event
=>
{
if
(
!
this
.
props
.
loggedIn
)
{
this
.
props
.
pushModal
(
<
LogInFirstModal
inOrderTo
=
"edit this calendar"
/>
);
return
;
}
const
target
=
event
.
target
;
invariant
(
target
instanceof
HTMLTextAreaElement
,
'target not textarea'
);
this
.
setState
({
text
:
target
.
value
},
this
.
updateHeight
);
};
onKeyDown
:
(
event
:
SyntheticKeyboardEvent
<
HTMLTextAreaElement
>
)
=>
void
=
event
=>
{
if
(
event
.
key
===
'Escape'
)
{
invariant
(
this
.
textarea
instanceof
HTMLTextAreaElement
,
'textarea ref not set'
,
);
this
.
textarea
.
blur
();
}
};
dispatchSave
(
serverID
:
?
string
,
newText
:
string
)
{
if
(
this
.
currentlySaving
===
newText
)
{
return
;
}
this
.
currentlySaving
=
newText
;
if
(
newText
.
trim
()
===
''
)
{
// We don't save the empty string, since as soon as the element loses
// focus it'll get deleted
return
;
}
if
(
!
serverID
)
{
if
(
this
.
creating
)
{
// We need the first save call to return so we know the ID of the entry
// we're updating, so we'll need to handle this save later
this
.
needsUpdateAfterCreation
=
true
;
return
;
}
else
{
this
.
creating
=
true
;
}
}
if
(
!
serverID
)
{
void
this
.
props
.
dispatchActionPromise
(
createEntryActionTypes
,
this
.
createAction
(
newText
),
);
}
else
{
void
this
.
props
.
dispatchActionPromise
(
saveEntryActionTypes
,
this
.
saveAction
(
serverID
,
newText
),
);
}
}
async
createAction
(
text
:
string
)
:
Promise
<
CreateEntryPayload
>
{
const
localID
=
this
.
props
.
entryInfo
.
localID
;
invariant
(
localID
,
"if there's no serverID, there should be a localID"
);
const
curSaveAttempt
=
this
.
nextSaveAttemptIndex
++
;
this
.
guardedSetState
({
loadingStatus
:
'loading'
});
try
{
const
response
=
await
this
.
props
.
createEntry
({
text
,
timestamp
:
this
.
props
.
entryInfo
.
creationTime
,
date
:
dateString
(
this
.
props
.
entryInfo
.
year
,
this
.
props
.
entryInfo
.
month
,
this
.
props
.
entryInfo
.
day
,
),
threadID
:
this
.
props
.
entryInfo
.
threadID
,
localID
,
calendarQuery
:
this
.
props
.
calendarQuery
(),
});
if
(
curSaveAttempt
+
1
===
this
.
nextSaveAttemptIndex
)
{
this
.
guardedSetState
({
loadingStatus
:
'inactive'
});
}
this
.
creating
=
false
;
if
(
this
.
needsUpdateAfterCreation
)
{
this
.
needsUpdateAfterCreation
=
false
;
this
.
dispatchSave
(
response
.
entryID
,
this
.
state
.
text
);
}
if
(
this
.
needsDeleteAfterCreation
)
{
this
.
needsDeleteAfterCreation
=
false
;
this
.
dispatchDelete
(
response
.
entryID
,
false
);
}
return
response
;
}
catch
(
e
)
{
if
(
curSaveAttempt
+
1
===
this
.
nextSaveAttemptIndex
)
{
this
.
guardedSetState
({
loadingStatus
:
'error'
});
}
this
.
currentlySaving
=
null
;
this
.
creating
=
false
;
throw
e
;
}
}
async
saveAction
(
entryID
:
string
,
newText
:
string
,
)
:
Promise
<
SaveEntryPayload
>
{
const
curSaveAttempt
=
this
.
nextSaveAttemptIndex
++
;
this
.
guardedSetState
({
loadingStatus
:
'loading'
});
try
{
const
response
=
await
this
.
props
.
saveEntry
({
entryID
,
text
:
newText
,
prevText
:
this
.
props
.
entryInfo
.
text
,
timestamp
:
Date
.
now
(),
calendarQuery
:
this
.
props
.
calendarQuery
(),
});
if
(
curSaveAttempt
+
1
===
this
.
nextSaveAttemptIndex
)
{
this
.
guardedSetState
({
loadingStatus
:
'inactive'
});
}
return
{
...
response
,
threadID
:
this
.
props
.
entryInfo
.
threadID
};
}
catch
(
e
)
{
if
(
curSaveAttempt
+
1
===
this
.
nextSaveAttemptIndex
)
{
this
.
guardedSetState
({
loadingStatus
:
'error'
});
}
this
.
currentlySaving
=
null
;
if
(
e
instanceof
ServerError
&&
e
.
message
===
'concurrent_modification'
)
{
const
onRefresh
=
()
=>
{
this
.
setState
({
loadingStatus
:
'inactive'
},
this
.
updateHeight
);
this
.
props
.
dispatch
({
type
:
concurrentModificationResetActionType
,
payload
:
{
id
:
entryID
,
dbText
:
e
.
payload
?
.
db
},
});
this
.
props
.
popModal
();
};
this
.
props
.
pushModal
(
<
ConcurrentModificationModal
onRefresh
=
{
onRefresh
}
/>
,
);
}
throw
e
;
}
}
onDelete
:
(
event
:
SyntheticEvent
<
HTMLAnchorElement
>
)
=>
void
=
event
=>
{
event
.
preventDefault
();
if
(
!
this
.
props
.
loggedIn
)
{
this
.
props
.
pushModal
(
<
LogInFirstModal
inOrderTo
=
"edit this calendar"
/>
);
return
;
}
this
.
dispatchDelete
(
this
.
props
.
entryInfo
.
id
,
true
);
};
dispatchDelete
(
serverID
:
?
string
,
focusOnNextEntry
:
boolean
)
{
const
{
localID
}
=
this
.
props
.
entryInfo
;
void
this
.
props
.
dispatchActionPromise
(
deleteEntryActionTypes
,
this
.
deleteAction
(
serverID
,
focusOnNextEntry
),
undefined
,
{
localID
,
serverID
},
);
}
async
deleteAction
(
serverID
:
?
string
,
focusOnNextEntry
:
boolean
,
)
:
Promise
<?
DeleteEntryResult
>
{
invariant
(
this
.
props
.
loggedIn
,
'user should be logged in if delete triggered'
,
);
if
(
focusOnNextEntry
)
{
this
.
props
.
focusOnFirstEntryNewerThan
(
this
.
props
.
entryInfo
.
creationTime
);
}
if
(
serverID
)
{
return
await
this
.
props
.
deleteEntry
({
entryID
:
serverID
,
prevText
:
this
.
props
.
entryInfo
.
text
,
calendarQuery
:
this
.
props
.
calendarQuery
(),
});
}
else
if
(
this
.
creating
)
{
this
.
needsDeleteAfterCreation
=
true
;
}
return
null
;
}
onHistory
:
(
event
:
SyntheticEvent
<
HTMLAnchorElement
>
)
=>
void
=
event
=>
{
event
.
preventDefault
();
this
.
props
.
pushModal
(
<
HistoryModal
mode
=
"entry"
dayString
=
{
dateString
(
this
.
props
.
entryInfo
.
year
,
this
.
props
.
entryInfo
.
month
,
this
.
props
.
entryInfo
.
day
,
)}
currentEntryID
=
{
this
.
props
.
entryInfo
.
id
}
/>
,
);
};
}
export
type
InnerEntry
=
Entry
;
const
ConnectedEntry
:
React
.
ComponentType
<
BaseProps
>
=
React
.
memo
<
BaseProps
>
(
function
ConnectedEntry
(
props
)
{
const
{
threadID
}
=
props
.
entryInfo
;
const
unresolvedThreadInfo
=
useSelector
(
state
=>
threadInfoSelector
(
state
)[
threadID
],
);
const
threadInfo
=
useResolvedThreadInfo
(
unresolvedThreadInfo
);
const
loggedIn
=
useSelector
(
state
=>
!!
(
state
.
currentUserInfo
&&
!
state
.
currentUserInfo
.
anonymous
&&
true
),
);
const
calendarQuery
=
useSelector
(
nonThreadCalendarQuery
);
const
keyserverID
=
extractKeyserverIDFromID
(
threadID
);
const
connection
=
useSelector
(
connectionSelector
(
keyserverID
));
invariant
(
connection
,
`keyserver
${
keyserverID
}
missing from keyserverStore`
,
);
const
online
=
connection
.
status
===
'connected'
;
const
callCreateEntry
=
useCreateEntry
();
const
callSaveEntry
=
useSaveEntry
();
const
callDeleteEntry
=
useDeleteEntry
();
const
dispatchActionPromise
=
useDispatchActionPromise
();
const
dispatch
=
useDispatch
();
const
modalContext
=
useModalContext
();
return
(
<
Entry
{...
props
}
threadInfo
=
{
threadInfo
}
loggedIn
=
{
loggedIn
}
calendarQuery
=
{
calendarQuery
}
online
=
{
online
}
createEntry
=
{
callCreateEntry
}
saveEntry
=
{
callSaveEntry
}
deleteEntry
=
{
callDeleteEntry
}
dispatchActionPromise
=
{
dispatchActionPromise
}
dispatch
=
{
dispatch
}
pushModal
=
{
modalContext
.
pushModal
}
popModal
=
{
modalContext
.
popModal
}
/>
);
},
);
export
default
ConnectedEntry
;
File Metadata
Details
Attached
Mime Type
text/x-java
Expires
Sun, Dec 7, 7:54 AM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
5834093
Default Alt Text
entry.react.js (15 KB)
Attached To
Mode
rCOMM Comm
Attached
Detach File
Event Timeline
Log In to Comment