-
-
Notifications
You must be signed in to change notification settings - Fork 44
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support variable distribution period length #1691
Support variable distribution period length #1691
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nicely done @snowteamer!
Preliminary review ready for you! I didn't have time to do a full review this time and had to top short after reviewing periodStampGivenDate
, but did my best to get what I had out to you.
frontend/model/contracts/group.js
Outdated
recentDate = recentDate.toISOString() | ||
} | ||
if (typeof recentDate !== 'string') recentDate = recentDate.toISOString() | ||
if (!isPeriodStamp(recentDate)) throw new TypeError('must be date or isostring') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This check doesn't make sense, because recentDate
is conceptually not a period stamp, it's a date, so please remove it.
You can convert a date to a periodstamp using dateToPeriodStamp
if you need to.
The reason why we have these functions and must use them is because even though dates and period stamps happen to be the same thing, they are conceptually different. Treating them as the different types that they are and using the corresponding conversation functions allows us to change the format of a period stamp in the future if we need to.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, and in fact I was looking for an isIsoString
function, but there is no such function in time.js
, and adding one would have duplicated the code in isPeriodStamp
. So I just used that
frontend/model/contracts/group.js
Outdated
// Only extrapolate one period length in the future. | ||
const extrapolatedDistributionDate = addTimeToDate( | ||
distributionDate, distributionPeriodLength | ||
).toISOString() | ||
if (recentDate >= extrapolatedDistributionDate) { | ||
return dateToPeriodStamp(extrapolatedDistributionDate) | ||
} | ||
return dateToPeriodStamp(distributionDate) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code should be replaced by the previous logic we had in the periodStampGivenDate
function in time.js
, so that it can extrapolate multiple periods out. There's no reason not to, and we have the code for that already.
I would recommend re-introducing that function in time.js
, and passing in sortedPeriodKeys
to it. That would keep this function here short & sweet while having the messy logic in time.js
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe you're right, but I haven't seen any use case where extrapolating more than one period in the future would be actually useful. I thought it didn't make much sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Say the user has their clock accidentally incorrectly set a year in the past. They will be browsing old payments they sent and using this getter to fetch the period in which the payment occurred. This getter will return the wrong value. Why should we return the wrong value when I've already written the code to return the correct value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This block is only called when recentDate >= distributionDate
, and I don't think distributionDate
can depend on the user's clock.
Therefore in case the clock incorrectly thinks it's already several period lengths in the future, way past the distribution date, so that this block gets called, then there just won't be any matching payment to be displayed but I think that's correct.
If the user has their clock accidentally incorrectly set a year in the past, so that recentDate
is one year lower than expected, then this block won't be called. The sorted periods will be searched instead, and I think the user should still be able to browse any stored payment period up to the distribution date.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, ok, that does seem to be true. Still, I don't see why we can't include a loop here that was already written, and therefore not need to worry about this returning an incorrect value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated to handle future dates using the period length in a loop like before (re-using periodStampGivenDate
in time.js
)
cf46ddc
to
2a40bdd
Compare
2a40bdd
to
7035686
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice work @snowteamer! Preliminary review ready!
frontend/model/contracts/group.js
Outdated
// Only extrapolate one period length in the future. | ||
const extrapolatedDistributionDate = addTimeToDate( | ||
distributionDate, distributionPeriodLength | ||
).toISOString() | ||
if (recentDate >= extrapolatedDistributionDate) { | ||
return dateToPeriodStamp(extrapolatedDistributionDate) | ||
} | ||
return dateToPeriodStamp(distributionDate) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Say the user has their clock accidentally incorrectly set a year in the past. They will be browsing old payments they sent and using this getter to fetch the period in which the payment occurred. This getter will return the wrong value. Why should we return the wrong value when I've already written the code to return the correct value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor change requests for tonight, still reviewing...
98d28d4
to
11b4f90
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Impressive work, @snowteamer. I didn't fully review this PR yet, but I did a preliminary review.
frontend/model/contracts/group.js
Outdated
for (let i = 1; i < sortedPeriodKeys.length; i++) { | ||
if (recentDate < sortedPeriodKeys) return sortedPeriodKeys[i - 1] | ||
} | ||
// This should not happen |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should be if (recentDate < sortedPeriodKeys[i]) return sortedPeriodKeys[i - 1]
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, can't believe no test caught this - thanks
getStartDate () { | ||
return this.periodStampGivenDate(new Date()) | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this currentPaymentPeriod
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be but currentPaymentPeriod
is a bit special in that it's controlled by reactiveDate, which doesn't update in real time but in interval. So if I understand correctly it will sometimes be wrong relative to the actual current date, therefore I chose to not rely on it in other computed properties
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@snowteamer Alex is right, because Vue.js is not going to automatically update the UI when new Date()
moves us into a new period. This is for 2 reasons:
- This is a method, not a computed property
- Even if it were a computed property, it still wouldn't work because computed properties are only updated in response to reactive changes. Since
new Date()
is not a reactive property itself, and is not updated as a result of a reactive Vue.js-related change (asreactiveDate
is), the computed property doesn't get updated either because that code is never re-run
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another preliminary review. Will wait until everything is addressed in previous reviews before proceeding with another review
frontend/model/contracts/group.js
Outdated
groupSortedPeriodKeys (state, getters) { | ||
// The .sort() call might be only necessary in older browser which don't maintain object key ordering. | ||
// A comparator function isn't required for now since our keys are ISO strings. | ||
return Object.keys(getters.currentGroupState.paymentsByPeriod ?? {}).sort() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DRY: replace getters.currentGroupState.paymentsByPeriod ?? {}
with getters.groupPeriodPayments
which does the same thing.
frontend/model/contracts/group.js
Outdated
const { distributionDate, distributionPeriodLength } = getters.groupSettings | ||
if (!distributionDate) return | ||
// This is not always the current period stamp. | ||
const distributionDateStamp = dateToPeriodStamp(distributionDate) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need to do this conversion, because distributionDate
is not a Date
object. It is in fact a period stamp that you can use directly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought distributionDate
was an ISO date string, which happens to also be a period stamp but is conceptually a different thing (as you explained to me). It made sense to me since distributionDate
comes from the group settings, whereas "period stamp" is a more internal type I think.
So anywhere I had the chance to compare a distributionDate
value against a date directly, I didn't do any conversion.
I will likely now have to add a few of such conversions there if I start to consider distributionDate
as being of the "period stamp" type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@snowteamer I think we made a mistake in how we named that variable... it is in fact a period stamp... and it happens to be a date represented as an ISO string.
Maybe you can add a comment to the constructor to that effect? (Renaming it now is not a good idea since there are live groups running).
Also, it's a bit confusing with how we've implemented dateToPeriodStamp
, which takes either a Date
or a string
:
export function dateToPeriodStamp (date: string | Date): string {
return new Date(date).toISOString()
}
And yet dateFromPeriodStamp
only returns a Date
:
export function dateFromPeriodStamp (daystamp: string): Date {
return new Date(daystamp)
}
But yeah, in the end, distributionDate
should be treated as a period stamp :-\
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, thanks for explaining! 😄
frontend/model/contracts/group.js
Outdated
else return | ||
} | ||
if (periodStamp === distributionDateStamp) { | ||
if (sortedPeriodKeys.length) { | ||
// If the distribution date doesn't match the latest known period stamp, | ||
// then either that stamp is for the waiting period, | ||
// or the distribution date has just been updated. | ||
// In both cases we can return it. | ||
if (latestKnownStamp !== distributionDateStamp) return latestKnownStamp | ||
// Otherwise it's a normal period, therefore substracting the period length would not be reliable. | ||
else return sortedPeriodKeys[sortedPeriodKeys.length - 2] ?? undefined | ||
} else { | ||
// If no period has been stored yet, then we're in the waiting period and can do arithmetic. | ||
return dateToPeriodStamp(addTimeToDate(distributionDate, -distributionPeriodLength)) | ||
} | ||
} | ||
const index = sortedPeriodKeys.indexOf(periodStamp) | ||
if (index === -1) { | ||
// Maybe the given stamp is wrong and has no associated period, | ||
// but is one period length ahead of a known one. | ||
// TODO: just return 'undefined' when sure it's always safe. | ||
const maybePreviousStamp = dateToPeriodStamp( | ||
addTimeToDate(dateFromPeriodStamp(periodStamp), -distributionPeriodLength) | ||
) | ||
return sortedPeriodKeys.includes(maybePreviousStamp) ? maybePreviousStamp : undefined | ||
} | ||
// If index is 0 then the caller will have to check the archive. | ||
if (index === 0) return | ||
return sortedPeriodKeys[index - 1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not understand what is going on here or why this code is so complicated.
AFAICT this is what this function should be:
periodBeforePeriod (state, getters) {
return (periodStamp: string) => {
const { distributionPeriodLength } = getters.groupSettings
const sortedPeriods = getters.groupSortedPeriodKeys
for (const i = sortedPeriods.length - 1; i >= 0; --i) {
const latestPeriod = sortedPeriods[i]
if (periodStamp >= latestPeriod) return latestPeriod
}
return dateToPeriodStamp(addTimeToDate(dateFromPeriodStamp(periodStamp), -distributionPeriodLength))
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code is indeed a bit complicated, in part because of the periodStamp === distributionDateStamp
case, so I've tried to explain it with a a lot of comments (but failed to make it clear enough)
this is what this function should be:
If I'm not mistaken, this won't ever return undefined
to defer searching to the archive code. And since every archived period could have a different length, simply substracting distributionPeriodLength
is not correct in that case.
Also it doesn't do anything special for the aforementioned special case.
Would be great if that case was actually not special and we could just use a simple loop like yours, but for now the suggested code fails this assertion line 118 in group-paying.spec.js
expect(periodBeforePeriod(onePeriodLengthAhead) === distributionDate).to.be.true
(not 100% sure why but my guess it's because in that case there exists a stored object for the waiting period but not for the current distribution date yet)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, that's a good point regarding distributionDate
- my code doesn't check for that because it assumes (incorrectly) that that is already stored in sortedPeriods
, which you're absolutely right, it might not be.
Also, my code has a bug related to this check:
if (periodStamp >= latestPeriod) return latestPeriod
I think that should be:
if (periodStamp > latestPeriod) return latestPeriod
The code that's currently here in the PR needs to be changed for a few reasons:
- As discussed above, we need to avoid passing
distributionDate
todateToPeriodStamp
- It needs to be simplified similarly to what I wrote above. Right now it's got some very questionable lines, like this:
if (periodStamp > distributionDateStamp) {
// Only extrapolate one period length in the future.
const onePeriodLenghtAhead = addTimeToDate(distributionDate, distributionPeriodLength)
if (periodStamp === dateToPeriodStamp(onePeriodLenghtAhead)) return distributionDate
else return
}
This is very strange. The purpose of this function is to return the period before a given period stamp. That's not what the lines above do at all. Instead they seemingly randomly add a period length to distributionDate
and compare periodStamp
to that, and then bails if the check fails.
We need something conceptually equivalent to the code I wrote above that:
- Takes into account
distributionDate
- Has a consistent mental model for how period stamps are created and iterated forward/backward
Mental model
Our mental model is as follows:
<--- [ UNKNOWN PAST ] --- [ SORTED_KEYS ] ---- [ UNKOWN FUTURE ] --->
Here we see 3 section. There is a "known" timeline in the center, and an unknown timeline at each end.
If we are given a period stamp that is inside of [ SORTED_KEYS ]
, here's what iterating it looks like:
[ A ] <-> [ B ] <-> [ C ]
^
|
periodStamp
If we want to go forward in time from periodStamp
pointing at [ B ]
stamp, then periodAfterPeriod
points us to [ C ]
and periodBeforePeriod
points us to [ A ]
.
If, however, we have this situation:
[ B ] <-> [ C ] <-> [ UNKNOWN FUTURE ]
^
|
periodStamp
Then:
periodAfterPeriod
addsdistributionPeriodLength
toperiodStamp
and returns the resultperiodBeforePeriod
returns[ B ]
And, if we have this situation:
[ UNKNOWN PAST ] <-> [ A ] <-> [ B ]
^
|
periodStamp
Then:
periodAfterPeriod
returns[ B ]
periodBeforePeriod
returnsundefined
Note: that this is the only scenario where undefined
can be returned.
Also note: in that case historicalPeriodBeforePeriod
(please rename from currently called getPeriodBeforePeriod
) does the exact same logic as periodBeforePeriod
, except it performs the check on a version of [ SORTED_KEYS ]
that includes historical payments from then archive.
In fact, because both periodBeforePeriod
and historicalPeriodBeforePeriod
contain line-by-line exactly the same logic, please create a shared function inside of time.js
that does this logic. When historicalPeriodBeforePeriod
and periodBeforePeriod
call that function, they will include an array containing the [ SORTED_KEYS ]
.
Finally, to take into account the distributionDate
, simply add in the distributionDate
to the [ SORTED_KEYS ]
that you pass in to the internal periodBeforePeriod
inside of time.js
.
And similarly, there will be a corresponding shared periodAfterPeriod
in time.js
that is called from both group.js
and PaymentsMixin.js
.
Hope that's clear! Let me know if you have any questions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is very strange. The purpose of this function is to return the period before a given period stamp.
Exactly, so if the given date is not actually a past, current or future period stamp but just a random ISO string, my assumption was that undefined
had to be returned.
If that assumption shoud be dropped so that these getters behave like periodStampGivenDate
, e.g. periodAfterPeriod
returns B
for any date between A
and B
not just for A
, then maybe they could as well be renamed for consistency:
- nextPeriodStampGivenDate
- periodStampGivenDate
- previousPeriodStampGivenDate
(I'd rather use -forDate than -givenDate). OtherwiseperiodAfter/BeforeDate
also sound good to me.
Another assumption in this PR, is that distributionPeriodLength
is now a variable setting whose value only holds for the current period. It can no longer be safely used to iterate between existing periods, but is just a hint to initially setup a default next distribution date.
Moreover, in the waiting period the distribution date i.e. the next period stamp can be updated without updating distributionPeriodLength
accordingly
}, | ||
|
||
// Note: 'recentDate' is a confusing name, as it can be in the future, or far in the past. | ||
async getPeriodStampGivenDate (givenDate: string | Date) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rename to historicalPeriodStampGivenDate
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It can also return non-historical results, so I think it would be a bit confusing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, but the point is that it unlike periodStampGivenDate
it can give historical payment periods, so that's why it needs to be named that way. See also the mental mode comment I just posted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not fan of dropping the get-
prefix here, as all other methods in this file are get-
prefixed. It likely helps to distinguish them from computed properties. But I also think getHistoricalPeriodStampGivenDate
would be a tad long...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should all PaymentsMixin.js
methods that can return both in-memory and historical results be named that way (some currently don't)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah feel free to make it consistent and clear 👍
(I haven't checked your latest changes as of this comment but will have a look now)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Finished my review. Nice work, @snowteamer
7f5c7ac
to
0361950
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another preliminary review ready!
// Append the waiting period stamp if necessary. | ||
if (!keys.length && MAX_SAVED_PERIODS > 0) { | ||
keys.push(dateToPeriodStamp(addTimeToDate(distributionDate, -distributionPeriodLength))) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note: !keys.length
should always be false because the contract constructor calls initFetchPeriodPayments
, ensuring at least one entry
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact, initFetchPeriodPayments
calls periodStampGivenDate
before doing anything else, which in turn calls groupSortedPeriodKeys
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I forgot that, that's a good point. 👍
I investigated this further and surprisingly yes this code actually seems to reproduce the behavior of the code on master
, which also initializes state.paymentsByPeriod
with the periodstamp of the period before the first distributionDate
— the never-used "waiting period". Which is kind of surprising but yeah this code seems to do the same thing so 👍
frontend/model/contracts/group.js
Outdated
@@ -1286,8 +1276,11 @@ sbp('chelonia/defineContract', { | |||
process ({ meta }, { state, getters }) { | |||
const period = getters.periodStampGivenDate(meta.createdDate) | |||
const current = getters.groupSettings?.distributionDate | |||
|
|||
if (current !== period) { | |||
const inWaitingPeriod = !current || meta.createdDate < current |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this logic captures whether or not we're in the waiting period.
By "waiting period" I assume you mean in the period before the very first distribution has started. However, this logic doesn't check for that. We could be in in the 3rd period since the group started and this would still evaluate to true.
Whether or not we're in the first waiting period is determined really by the number of keys in getter.groupPeriodPayments
. If there is only 1 key in there, and meta.createdDate < current
, only then can we be considered to be inWaitingPeriod
. And then you also don't need the redundant check of meta.createdDate !== current
below:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could be in in the 3rd period since the group started and this would still evaluate to true.
In a "normal" (not waiting) period, meta.createdDate < current
should always be false, since the distribution date is when the current period started, not when the next period will start
Otherwise if I'm wrong, couldn't I just use the new getters.groupDistributionStarted(meta.createdDate)
implemented by Alex?
Although I see it's not used alone somewhere else in this file, but combined with a check for the number of stored period keys:
if (getters.groupDistributionStarted(meta.createdDate) || Object.keys(getters.groupPeriodPayments).length > 1)) {
(not sure why the getter isn't enough to ensure we're out of the waiting period)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a "normal" (not waiting) period, meta.createdDate < current should always be false, since the distribution date is when the current period started, not when the next period will start
That's probably true.
couldn't I just use the new getters.groupDistributionStarted(meta.createdDate) implemented by Alex?
The two seem to be equivalent. You could do:
const inWaitingPeriod = !getters.groupDistributionStarted(meta.createdDate)
That would probably be best from a DRY perspective.
but combined with a check for the number of stored period keys:
Yeah I'm not sure what that's about. @Silver-IT couldn't you remove that || Object.keys(getters.groupPeriodPayments).length > 1
check? Although that might be there for a reason related to adjusting the distribution date that I don't remember.
Yeah, it could be that the distribution date was adjusted via a proposal to suddenly be in the past?
This is the code: https://github.com/okTurtles/group-income/pull/1643/files#diff-5c6306168381a1d6902fce7da1cdf9a6811e2ed4ac74d7d97bfff5145087b626R122
It seems like the proposal only allows for updating the distribution date to a date in future, starting with 1 day in the future.
However, what if by the time the proposal passes we are already ahead of that date? For example, what today is Sept. 1st, and we set the distribution date to Sept 30th, but then create a proposal to make it Sept. 2nd, but by the time the proposal passes it's already Sept 3rd?
Need to consider this.... 🤕
const plusOnePeriodLength = (timestamp: string, periodLength: number): string => ( | ||
dateToPeriodStamp(addTimeToDate(timestamp, periodLength)) | ||
) | ||
|
||
const minusOnePeriodLength = (timestamp: string, periodLength: number): string => ( | ||
dateToPeriodStamp(addTimeToDate(timestamp, -periodLength)) | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These look useful, you could export these too...
if (typeof date === 'string' && !isIsoString(date)) { | ||
throw new TypeError('must be ISO string') | ||
} | ||
const timestamp = new Date(date).toISOString() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is redundant when date
is an iso string already. Perhaps this logic could be rewritten like so:
const timestamp = typeof date === 'string' ? date : date.toISOString()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed - This time I wanted to keep the code simple, but actually I don't like creating Date
objects when not necessary
date: Date | string, | ||
{ knownSortedStamps, periodLength }: { knownSortedStamps: string[], periodLength: number } | ||
): Object { | ||
if (typeof date === 'string' && !isIsoString(date)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be rewritten to remove the redundant typeof date === 'string'
check like so:
if (!(isIsoString(date) || date instanceof Date)) {
(Note: for obsessive-compulsive "performance" reasons I've placed the check that's most likely to pass first, in this case the parameter is more likely to be an ISOString, so that check happens first to avoid triggering the instanceof Date
check)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, and I would even remove the date instanceof Date
check if possible, since Flow annotations should already handle that.
Initially I added the isIsoString
check only because string was are a superset of ISO strings
Edit: After further testing, it appears that .toISOString()
throws when called on invalid date objects like new Date('foo'). So to avoid possible uncaught errors here we'll have to do a bit more validation or use a try
block...
What about this:
// `isNaN` - not `Number.isNaN` - is used here to catch `Invalid Date` objects.
if (!isIsoString(date) && !(date instanceof Date && !isNaN(date))) {
throw new TypeError('must be ISO string or valid Date object')
}
const timestamp = typeof date === 'string' ? date : date.toISOString()
Or maybe we could restrict the API to just not accept Date objects at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why you changed the condition. The one I used should work fine, and it also avoids both checks most of the time because it uses ||
The flow annotations won't always save us (type info is lost when using SBP)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this suggestion I used &&
rather than ||
to avoid an extra pair or parens, and isNaN
to guard against Invalid Date
values. These are quite uncommon but could cause the toISOString()
call to throw
Otherwise it's equivalent to if (!(isIsoString(date) || date instanceof Date))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
used && rather than || to avoid an extra pair or parens
It's better to have the parens than to have an extra logical check imo
and isNaN to guard against Invalid Date values.
I really don't think that's needed
Otherwise it's equivalent to if (!(isIsoString(date) || date instanceof Date))
It's not... the ||
condition results in 1 check most of the time, whereas the modified &&
results in 3 different check every time
current = knownSortedStamps[i] | ||
// `i + 1` is always a valid index. | ||
next = knownSortedStamps[i + 1] | ||
previous = knownSortedStamps[i - 1] ?? undefined |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- We don't need to
?? undefined
here (undefined
is returned for us) - Don't forget to
break
out of the loop
}, | ||
mounted () { | ||
this.updateHistory() | ||
}, | ||
methods: { | ||
async updateHistory () { | ||
this.history = await Promise.all(this.periods.map(async (period, i) => { | ||
const periods = await this.getSortedPeriodKeys() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this.getSortedPeriodKeys
doesn't exist!
(Please test code before pushing to make sure it works. I might not always be a good enough of a reviewer to catch stuff like this, but the computer will always catch it if you test these cases)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well I ran our test suite - I'm unfortunately no better at catching typos or old names, and need my tools to do it for me.😅
getStartDate () { | ||
return this.periodStampGivenDate(new Date()) | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@snowteamer Alex is right, because Vue.js is not going to automatically update the UI when new Date()
moves us into a new period. This is for 2 reasons:
- This is a method, not a computed property
- Even if it were a computed property, it still wouldn't work because computed properties are only updated in response to reactive changes. Since
new Date()
is not a reactive property itself, and is not updated as a result of a reactive Vue.js-related change (asreactiveDate
is), the computed property doesn't get updated either because that code is never re-run
getDueDate () { | ||
return this.dueDateForPeriod(this.currentPaymentPeriod) | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is supposed to be a computed property, not a method. Methods and computed properties are different. When computed properties change the UI gets updated automatically. A method cannot be bound to something as a computed property and won't result in UI updates when its return value changes. Check the Vue.js docs for more info.
data: { | ||
relativeTo: null | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please check the Vue.js documentation for how to set data
properly. data
cannot be an object, it must be a function that returns an object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, good idea, my Vue.js is visibly a bit rusty...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@snowteamer I thought I might be able to merge this PR but actually there are some serious errors still (testing still TBD).
Flow errors
PRs must never introduce flow errors. The following errors are present:
Running "exec:flow" (exec) task
Error -------------------------------------------------------------------- frontend/model/contracts/shared/time.js:25:47
Cannot get `Object.prototype.toString` because property `toString` [1] cannot be unbound from the context [2] where it
was defined. [method-unbinding]
frontend/model/contracts/shared/time.js:25:47
25| if (!(isIsoString(date) || Object.prototype.toString.call(date) === '[object Date]')) {
^^^^^^^^ [1]
References:
/private/tmp/flow/flowlib_e73d01e77da1cc86_501/core.js:236:5
236| toString(): string;
^^^^^^^^^^^^^^^^^^ [2]
If you're certain this code is correct then you can use FlowFixMe comments to disable flow checking on that line (search the project for examples if you need).
Contract files are incorrect
Running grunt pin:0.1.8 --overwrite
shows that the generated contracts are in fact different from the changes in this PR, indicating that they're out of date. Whenever you modify contracts that command needs to be run before pushing a commit.
Boyscoutting
I left some other comments earlier (e.g. see the comment about mounted
placement, this.humanDate
instead of humanDate
), etc. that can now be addressed since these other changes are being requested.
While you're working on these changes I'll also be testing this PR and will get back to you with any other issues I find (if any).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So far the tests have gone pretty well!
I did run into one problem though:
To reproduce:
-
Create a $1000 group with u1 and invite u2, u3
-
u1 income details pledging $100, u2 making $800, u3 making $780
-
In console enter:
sbp('gi.actions/group/forceDistributionDate', { contractID: sbp('state/vuex/state').currentGroupId })
-
Make 1 full payment, and 1 partial payment
-
Move to the next distribution period by running
sbp('gi.actions/group/forceDistributionDate', { contractID: sbp('state/vuex/state').currentGroupId })
again -
As u1 check the completed payments
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two issues:
- The contracts are still out of sync with the latest changes. Please run
grunt pin:0.1.8 --overwrite
each time any modifications are made to contracts before pushing a commit - The Record Payments modal displays 2 rows for some reason in the 2nd payment period:
To reproduce:
- Create group with u1 and u2, and add income details for both
- Run
sbp('gi.actions/group/forceDistributionDate', { contractID: sbp('state/vuex/state').currentGroupId })
to enter new period - u1 pays u2
- Run
sbp('gi.actions/group/forceDistributionDate', { contractID: sbp('state/vuex/state').currentGroupId })
to enter 2nd period - u1 pays u2 — modal shows 2 rows for some reason
const sent = [] | ||
const received = [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did this remove cloneDeep(this.ourPayments?.sent || [])
and cloneDeep(this.ourPayments?.received || [])
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To avoid collecting the same entries twice since the current period is going to be processed anyway in the below loop. Another idea would be to keep the cloneDeep
calls, but then exclude the current period from the loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WOOHOOOOOO!!!! 😄 🎉
GREAT JOB @snowteamer !!!!!
This was a doozy of a PR but you got it working!! 😄 🥇 🏆
* Fix issues with chatroom, move some key logic into selectors * Rename selector to isWaitingForKeyShare * fix: update description for group channels * WIP - Fix manifest not found error upon contract file change (#1703) * Fix manifest not found error on contract file change * Mark the gruntfile as WIP * Remove WIP tag and code duplication * Fix Chat UI bugs (#1712) * fix: error of scrolling outside of the ChatMain section * feat: jump to the latest when resize window * feat: jump to the latest message without any delay when resize window * fix: auto scroll 25 times a second * File attachment to chat - 1st chunk (#1694) * add file attachment button to the chat input-area el * dnd mixin skeleton * remove trailing space * create the image preview vue component. * task 1-2. create a preview file component for a non-image attachment * build 'File too large' modal and pop it out when size is too large * add a compressed svg image for attachment / make sure it can be colored via css * add links for the drag&drop references * display overlay image when chat-area is dragged over * hook up DnD behavior with file-attachements method * resolving some of Greg's CR * another chunk of work for Greg's CR * work on Greg's CR / multiple file attachments * fix the test failure * make it possible to append attachments when drag&drop is performed * resolve the linter error * work on Greg's CR / fix the bug where dot is displayed * Add signedData, change message format * Re-add validation * Fix flow errors * Fix OP_ATOMIC handling * Harmonise types for signedData and encryptedData * Proposal to update distribution date (#1643) * feat: created a DistributionDate modal * chore: improve translation * feat: updated translation and added isInFirstDistributionPeriod * feat: completed the change distribution date workfloww * feat: restrict group members to change group settings * feat: improve updateSettings group action * feat: exception handler when open proposal modal directly * fix: error to close General Proposal modal * feat: improve DistributionDate proposal workflow * feat: proposal Item for distribution date * feat: simplified interactive message templates * feat: proposal to change distribution date * fix: cypress error with distribution updates * feat: resolved feedback from @sebin * feat: test case for changing distribution date * fix: travis error regarding distribution date * fix: disable submit button instead of close proposal modal * fix: groupDistributionStarted using Date * feat: simplified * fix: init scrollDistance when switch channel * fix: simplified payment periods config * fix: updated translation * chore: remove DRY issue and update translation * feat: localized texts for proposals * Implement inner signatures for OP_KEY_REQUEST * Inner signatures for actions * Add wrapper to encryptedData result * Fix flow errors * Fix rotate keys error when content is undefined * Fix JSON serialization issues for incoming encrypted data * Reintroduce toJSON, fix issue with () * Use additional data for encryption * Start adding user keys to group & chatrooms * Add direction attribute to GIMessage * Add comment * Simplified some of the code relating to GIMessage and signedData * Fixed reference to headJSON * Rudimentary support for inner signatures (missing permissions) * Various changes: * Light refactoring of `encryptedAction` to make it clearer - Actions to own own identity do not use inner signatures * Permission checking and enforcement for inner signatures - Now, permissions are checked in a uniform way in a central location * Add new `ourContactProfilesById` and `ourContactsById` getters * Changes for optinally encrypting OP_KEY_* * Fix key re-selection issue * Should not display archived proposals from other group (#1723) * feat: destructure archPropData * chore: reverted style changes * chore: changed variable name * chore: added comments * Create ci.yml * Rethrow uncaught Vue errors in dev mode or CI (#1708) * Rethrow uncaught Vue errors in dev mode or CI * Apply review * WIP: Use encrypted OP_KEY_* and new OP_KEY_REQUEST * Bugfixes * Merge PrivateDM and GroupDM (#1709) * feat: direct messages in identity contract * feat: add/remove getters * feat: simplified getters * wip: merging privateDMs and groupDMs * fix: coding issues * feat: UI updates for the DMs with more than 3 * fix: add member to dm * feat: changed base branch from e2e-protocol-ricardo to e2e-protocol * fix: error in get dm by usernames * chore: added comments * fix: error in function parameters declaration * fix: inconsistent naming of members when create a new DM * fix: redundant message rendering issue * feat: grun tpin * chore: removed wrong version of contracts * feat: grunt pin:0.1.8 * feat: added comments * chore: removed changes in chelonia * chore: added comments * chore: fix typo * feat: error handling * chore: removed unnecessary comment * feat: improved chatroom types and added comments * feat: simplified getter * chore: simplified * chore: added comments, error handling * feat: merged DMs from inside and outside of the group * feat: remove workflow regarding the dm visibility * Bugfixes for new OP_KEY_SHARE * feat: remove notifications when leave group (#1722) * Bugfixes * Improvements & bugfixes chelonia.js: Use regular signedOutgoingData when possible group.js: Fix name of selector * Bugfixes * Bugfixes for UI error handling. Move encryptedOutgoingData into /keyDel and /keyUpdate * Implement some PR feedback * Make KRS+KR atomic; disable deleting state when logging out * Fix flow issues * Mark contract as dirty until keys are received * Bugfix: call group join upon login * Add comments * Encrypted storage * chore: resolved conflicts * feat: giCreateGroup waits until the user joins the group * chore: fixed some errors * fix: error egarding Awesome button disability * Mark contract as dirty until keys are received * Bugfix: call group join upon login * Add comments * feat: group-chat-direct-message.spec.js test passed * Remove event listener when logging out * Use settings for key storage instead of SessionStorage * Refactoring: state encryption logic * Simplify check * Update references to session storage with app settings * Support variable distribution period length (#1691) * Fix Vue error in groupProposalSettings * Remove unused paymentTotalFromUserToUser getter * Add groupSortedPeriodKeys getter in group.js * Update getters to support variable period length * Add isIsoString in time.js and use it * Support variable period length in MonthOverview.vue * Apply review * Add periodStampsForDate in time.js * Add getHistoricalPaymentPeriods in PaymentMixin.js * Rename getSortedPeriodKeys to getAllSortedPeriodKeys * Add a few comments * Export new helpers from time.js * Improve validation in periodStampsForDate * Remove occurences of 'undefined' * Fix missing break statement * Fix wrong getter name in SupportHistory.vue * fixup! Improve validation in periodStampsForDate * Restore logging in Cypress * Fix reactivity issue in MonthOverview.vue * Fix data() in PaymentRowReceived/Sent.vue * Use groupDistributionStarted i/o inWaitingPeriod * Revert change in updateDistributionDate * Use humanStart/DueDate in MonthOverview.vue * Simplify PaymentDetail.vue/initializeDetails * Use mounted i/o watch in PaymentRowReceived/Sent.vue * Fix pedantic Flow error * Pin contracts to 0.1.8 * Move mounted() near the top * Revert humanDate to plain import in MonthOverview.vue * Use payment.period to fix Invalid Date errors * Add .start field in initPaymentPeriod * Add .end field in in-memory payment periods * Rename getPeriodPayment to getPaymentPeriod * Fix issue 1739 * Pin contracts * Add test for payment in 2nd period * Fix conditions for omitting empty key ops * EncryptedValue: ensure that it's a string * Refactor: logout handler * Convert password to passwordFn to prevent logging * Fix issue (key rotation / private channel not working after rejoin) * Update Style-Guide.md Added `User-facing Strings Guide` * Update Style-Guide.md typo * Update Style-Guide.md Improved translation section * Update Style-Guide.md * Update Style-Guide.md * Update Style-Guide.md * Fix: leaving private chat without being member * Update Style-Guide.md fix typos * Documentation * feat: updated test cases according to the updated invitation workflow * Update Style-Guide.md Consistent use of colons * #1742 Use 'submit-button' component for asynchronous actions (#1750) * add a todo comment for future <submit-button /> replacement * add submit-button component to various places * work on Greg's CR * #1704 - Add 'Export to CSV' feature to payments table (#1724) * add export CSV button to the table * create ExportPaymentsModal.vue and register it * make sure all the UI side of work are ready * complete CSV Extraction logic * complete the download CSV file logic * fix the linter error * work on Greg's feedback on the button position * make sure 'Export CSV' button is not overlapped regardless of the screen width * fix the linter error * put all-period option into the dropdown * remove the unecessary extra padding * DRY PaymentsMixin.js * update for Greg's CR on the PR * fix the broken translation o the modal title * restore package.json wtf... * Bugfixes * Add UI prompt to login and re-order buttons * Rename button names * Bugfix: null inner signing key when leaving chatroom * Add comments explaining the innerSigning attributes * Bugfix: race condition when joining due to skipActionProcessing * Change bad previous head log level to warn * Remove skipActionProcessing from respondToKeyRequests * wip: second half * feat: added profile-card in payments table * [e2e-protocol] Fix the welcome screen bug in /pending-approval page (#1748) * render welcome screen as position:fixed in /pending-approval page * make the selector higher priority * Fixed error to save scroll position (#1730) * feat: direct messages in identity contract * feat: add/remove getters * feat: simplified getters * wip: merging privateDMs and groupDMs * fix: coding issues * feat: UI updates for the DMs with more than 3 * fix: add member to dm * feat: changed base branch from e2e-protocol-ricardo to e2e-protocol * fix: error in get dm by usernames * chore: added comments * fix: error in function parameters declaration * fix: inconsistent naming of members when create a new DM * fix: redundant message rendering issue * feat: grun tpin * chore: removed wrong version of contracts * feat: grunt pin:0.1.8 * feat: added comments * chore: removed changes in chelonia * chore: added comments * chore: fix typo * feat: error handling * chore: removed unnecessary comment * feat: improved chatroom types and added comments * feat: simplified getter * chore: simplified * chore: added comments, error handling * fix: error to save scroll position * chore: removed allowDMInvite * Bugfix: key rotation on watched contracts * Fix message format for OP_KEY_SHARE * Rotate PEK when a group member leaves * Generic definition for the various DB stores * Revert changes to frontend/model/database.js and use the identity contract ID as key * Remove withEnv * fix: error regarding passwordFn * bug-fix for closing modal reacting too slowly * Handle encrypted and unencrypted invites (accounting) * fear: remove archived data when leave the group (#1747) * Add return * Improvements to respondToKeyRequests * fix: errors in cypress test * Make sync no-op for existing contracts * fix: cypress issues * fix: error after login * Listen for login event before calling postSubmit * Await in the login form, documentation * Updates to postSyncOps * Split up syncContractAndWatchKeys * Add chelonia/private/enqueuePostSyncOps to blacklist * Closure for storeSecretKeys to prevent logging secrets * Remove promiseAllSettled. Bugfixes. * Update okturtles.eventqueue * Side-effects without await * Fix invocation * Make sure we sync any identity contracts we haven't synced upon login (#1763) * feat: sync missing identity contracts after joining group * chore: remove IIFE * chore: resolved feedback * Remove await on /out in internals.js * Remove queueInvocation for /out events * Make cannot leave if not a member error into a warning * Show login error prompt only for errors related to the identity contract itself * Fix issue of removing two members (error on second removal) * Update identity.js login logic, remove spurious sideEffect check * pin contracts * Implement a way to set default groupId (#1773) * feat: set default group ID which is successfully joined * fix: recovered package.json * Fix inability to join after sharer signs out * Updated login error message (#1731) * feat: make login error message more informative * chore: added a line to log error * fix: error of Member Removal (#1764) * Fix infinite re-subscription due to foreign keys * Revert "Updated login error message (#1731)" This reverts commit cf4b7f4. * Updated login error message (#1731) * feat: make login error message more informative * chore: added a line to log error * Fix undefined in vmInvites * preSendCheck for outgoing messages; move hooks to publishEvent * Remove preSendCheckContract * Remove await from call to leaveAllChatRoomsUponLeaving --------- Co-authored-by: Ricardo Iván Vieitez Parra <[email protected]> Co-authored-by: Greg Slepak <[email protected]> Co-authored-by: snowteamer <[email protected]> Co-authored-by: Sebin Song <[email protected]>
Closes #1631
Closes #1662