Commit Graph

132 Commits

Author SHA1 Message Date
Jordan Rose
a87651e7ed Handle incoming group call rings
This adds a table of "cancelled group rings", to handle out-of-order
delivery between a cancellation and a ring. (This can happen when the
cancellation comes from a linked device rather than the original
ringer, though it's very unlikely.) Thirty-minute-old cancellations
are cleared lazily.

This also adds a new IncomingCallControls to replace the usual
CallControls in a GroupCallViewController.

And finally, there's a lot of code that was 1:1-call-specific that now
handles group calls as well, either by being more general or by
checking which kind of call we have.
2022-11-07 09:48:53 -08:00
Sasha Weiss
c7dd7f4c84
Actions for donation megaphone 2022-11-03 11:39:37 -07:00
Max Radermacher
9db9d94085
Handle isComplete during incoming contact syncs 2022-10-19 09:29:52 -07:00
Harry
201f002499
Add lastReadTimestamp to StoryContextAssociatedData
* Add last read timestamp to StoryContextAssociatedData

* Write lastReadTimestamp on StoryContextAssociatedData

* Copy lastViewedTimestamp over to lastReadTimestamp to simplify badge query

* apply didSets for data that changed at rest
2022-10-18 11:18:06 -07:00
Sasha Weiss
151e8b8044
Migrate from ExperienceUpgradeId to ExperienceUpgradeManifest (#5163) 2022-10-17 09:55:12 -07:00
Harry
74228f424e
Add StoryContextAssociatedData to compute story tab badge count without hidden stories
* Add StoryContextAssociatedData and db migrations

* Remove hideStory from ThreadAssociatedData

* Remove lastViewedStoryTimestamp and lastReceivedStoryTimestamp from TSThread

* drop deprecated columns in db migration

* add indexes

* dump schema.sql

* Update unviewed stories SQL query

* fix thread fetching for incoming story messages

* reload story tab badge when StoryContextAssociatedData changes

* Add test for TSGroupModel backwards-compatible deserialization

* move db migration

* Only use StoryContextAssociatedData for story badge count

* update StoryContextAssociatedData lastReceivedTime when a story message is deleted

* catch group threads for outgoing story messages too

* clean up sql query

* Only update lastReceievedStoryTimestamp for remote deletions

* add latestUnexpiredTimestamp to StoryContextAssociatedData
2022-10-03 21:16:01 -07:00
Harry Sanabria
ad65ef501f Add lastReceivedStoryTimestamp to schema.sql 2022-09-22 17:05:06 -07:00
Nora Trapp
9776699137 Sync when contacts become unregistered 2022-09-15 14:09:03 -07:00
Harry
de198f118b
Add story hidden state to ThreadAssociatedData
* Add story hidden state to ThreadAssociatedData

* make hideStory column non-null
2022-09-02 12:03:50 -07:00
Sasha Weiss
569cdb2aad
Durable jobs for leaving groups 2022-08-26 13:13:23 -07:00
Nora Trapp
89076ed822 Fix stories tab badging 2022-08-20 15:01:03 -07:00
Nora Trapp
1d9a8a8995 Add lastViewedStoryTimestamp to TSThread 2022-08-15 14:15:11 -07:00
Nora Trapp
edc5c7beae Fix schema.sql for 2022-08-08 14:34:51 -07:00
harry-signal
741c3961bd
Add audioPlaybackRate thread-level metadata to ThreadAssociatedData table 2022-08-01 15:34:40 -07:00
Evan Hahn
64462ac1b0 Save a receipt when you send someone a badge
This commit:

1. Adds a new donation receipt type: gifts.
2. Saves receipts when sending someone a gift.

The first part was the main challenge. Previously, donation receipts had
logic like this (pseudocode):

     def getReceiptType(self):
       if self.subscriptionLevel:
         return "subscription"
       else:
         return "one-time"

Now, we explicitly encode the type and have logic to handle "legacy"
receipts that don't have a type encoded.
2022-07-08 13:17:30 -05:00
Evan Hahn
f5556d1857
Use durable job for sending badge gifts
Previously, sending a gift badge was not a durable operation, which
meant that crashes/failures could cause users to have their payment
methods charged without actually sending the badge.

Now, the flow is split up into two steps: non-durable parts before the
charge is attempted, and durable parts afterward.

The high-level flow is:

1. Prepare the payment, which involves a couple of repeatable network
   requests.
2. Enqueue a job with the prepared payment, that:
   1. Charges the payment method (idempotently)
   2. Requests a receipt credential (idempotently)
   3. Enqueues a gift message, and optionally a text message
3. When the job completes, open the conversation in the UI.
2022-07-01 17:10:31 -05:00
Nora Trapp
7bd167f815
Initial story sending support
* Little fix for context menu

* Add 'My Stories' section to stories tab

* Add new story thread types

* Show stories in conversation picker

* Support for sending stories

* Update story list when sending stories

* Add basic 'My Stories' view controller

* Initial stories settings screens

* Consolidate TSPrivateStoryThread and TSMyStoryThread into one class

* Require an explicit read transaction to initialize an outgoing message

* Fix linting

* Allow enabling group story from internal settings

* Fix tests

* PR Feedback
2022-06-10 22:28:03 -04:00
Evan Hahn
8009206c18 Respect recipient capabilities when sending gift badges
This respects the `giftBadges` capability when trying to send gift
badges. In other words, it prevents you from sending gift badges to
someone who lacks the capability.

The bulk of this change involves fetching and saving of this new
capability. The rest of the code involves showing it on the "choose
recipient" screen (and some debug screens).
2022-06-10 22:37:22 +00:00
Max Radermacher
724ea55aa7 Add giftBadge property to messages
Also run `Scripts/sds_codegen/sds_codegen.sh`
2022-05-27 12:31:24 -07:00
Max Radermacher
da08463ade Add duration column to model_ProfileBadgeTable 2022-05-26 16:14:40 -07:00
Michelle Linington
98846da1f1 Adjust disappearing message index
The existing index was not useful for the query we needed. Running this
query would open a write transaction for several seconds on every
launch. This new index should be much faster.
2022-04-27 12:25:10 -07:00
Evan Hahn
8b072620a6 Add "Donation Receipts" view 2022-04-11 16:21:12 -05:00
Nora Trapp
702e38e7ff Break up index migrations over multiple releases 2022-04-01 16:21:21 -07:00
Nora Trapp
409e89e721 Add index for story reply counts 2022-03-31 01:52:54 -07:00
Nora Trapp
e662b099dd Check if a user is stories capable when sending group replies 2022-03-31 00:37:56 -07:00
Nora Trapp
4c6aa8de2d Add storyReactionEmoji column to TSMessage 2022-03-31 00:34:33 -07:00
Nora Trapp
e95e392494 Add story context to TSInteraction 2022-03-31 00:34:32 -07:00
Nora Trapp
d7f12d3f39 Convert StoryMessageRecord to use new SDSCodableModel 2022-03-16 17:54:38 -07:00
Nora Trapp
a656c91fcf Store the timestamp when a story was viewed 2022-03-16 17:53:28 -07:00
Nora Trapp
5673b73c3a Add HomeTabBarController 2022-03-16 17:52:41 -07:00
Nora Trapp
376471587f Backend support for stories receipt 2022-03-16 17:51:48 -07:00
Nora Trapp
edc89c3db1 Store badge receipt credential presentation on the durable job 2021-11-17 15:14:03 -08:00
Eugene Bistolas
78cb9352c3 [Badging] Durable receiptCredential fetch and redemption 2021-11-15 12:47:35 -10:00
Michelle Linington
ebadb12dad Minor comment tweaks, schema adjustments 2021-11-09 14:25:42 -08:00
Michelle Linington
d1f53b7151 Fix nullability of new OWSUserProfile property 2021-11-09 14:25:42 -08:00
Michelle Linington
fa0d10f047 Firm up database schema 2021-11-09 14:25:42 -08:00
Matthew Chen
997f354f74 Only warn in logs when duplicate decryptions occur. 2021-09-28 17:22:17 -03:00
Matthew Chen
73b019f8da Refine decrypt deduplication. 2021-09-23 23:22:26 -03:00
Matthew Chen
ad1bdf48d6 Refine decrypt deduplication. 2021-09-23 23:16:41 -03:00
Matthew Chen
59e33de962 Refine decrypt deduplication. 2021-09-23 22:49:44 -03:00
Matthew Chen
1b46d06336 De-duplicate by serverGuid. 2021-09-23 00:08:02 -03:00
Matthew Chen
026b02c267 Deduplication message decryption to avoid spurious errors when NSE and main app message processing race. 2021-09-23 00:08:02 -03:00
Michelle Linington
04c7d8eff4 Update schema definition 2021-09-22 14:05:13 -07:00
Matthew Chen
8aa213d25b Deduplication message decryption to avoid spurious errors when NSE and main app message processing race. 2021-09-22 12:05:28 -07:00
Michelle Linington
1012d7b42d Tune indices for more performant conversation loads
- Replaces the existing placeholder excluding index for two new indices,
  optimized to reduce disk I/O. These are partial indices, only
  applicable to the placeholder excluding queries performed during
  conversation load. They're also covering indices. Most queries will be
  resolved by only consulting the index. There's no need to consult the
  backing rows in the model_TSInteraction table.
- Adds a helper SQL bytecode explainer for debugging

Tracking down these optimizations took a great deal of time. I promised
in a comment that this commit message will have a thorough explanation
of why these indices are constructed the way they are. The TL;DR:
- There are a bunch of limitations on index column ordering that you
  should be aware of.
- Partial indices can help you avoid some of those limitations
- SQLite can be finnicky when trying to decide if a partial index is
  applicable to a query.
- When an index covers a given query, it can avoid a whole bunch of
  disk I/O.
- Index row size matters, and SQLite may choose a less efficient index
  if it looks like it might be smaller.

The long explanation of lessons learned along the way. All of this is
based on observation, documentation, and trial-and-error:

1. SQLite can be *very* strict when deciding whether or not to use a
   partial index. Compare these two index declarations:
  > CREATE INDEX index1 ON table(uniqueThreadId, id) WHERE 'recordType IS NOT 70';
  > CREATE INDEX index2 ON table(uniqueThreadId, id) WHERE recordType IS NOT 70;

  Note the only difference between the two partial index declarations is
  wrapping the WHERE clause in quotes. The GRDB index builder we were
  using before would do this on our behalf.

  Now let's run this query against both indices. The indices we declared
  *should* be a perfect choice for this query:
  > SELECT COUNT(*) FROM table WHERE uniqueThreadId = 'abc123'
  > AND id > 100 AND recordType IS NOT 70;

  With the first index:
  > `--SEARCH TABLE model_TSInteraction USING INDEX index_interactions_on_threadUniqueId_and_id (uniqueThreadId=? AND id>?)
  With the second index:
  > `--SEARCH TABLE model_TSInteraction USING INDEX index2 (uniqueThreadId=? AND id>?)

  By dropping the quotes around the WHERE clause, SQLite's query planner
  is now convinced that the partial index is applicable to the query
  we're running. This is good, since as SQLite iterates over the index,
  it can skip any recordType comparisons. It also reduces the search
  space, since it doesn't even need to read rows it should exclude.

2. Covering indices can be a huge performance win. An index is covering
   when every column in the index matches some element. Let's again
   compare some indices:
  > CREATE INDEX index1 ON table(uniqueThreadId, id) WHERE recordType IS NOT 70;
  > CREATE INDEX index2 ON table(uniqueThreadId, id, uniqueId) WHERE recordType IS NOT 70;
  > CREATE INDEX index3 ON table(uniqueThreadId, id, recordType, uniqueId) WHERE recordType IS NOT 70;
  (Why a difference between index2 and index3? See the next section)

  When run on this query:
  > SELECT uniqueId FROM table WHERE uniqueThreadId = 'abc123'
  > AND id > 100 AND recordType IS NOT 70;

  The first index:
  > --SEARCH TABLE model_TSInteraction USING INDEX index1 (uniqueThreadId=? AND id>?)
  The second index:
  > --SEARCH TABLE model_TSInteraction USING INDEX index2 (uniqueThreadId=? AND id>?)
  The third index:
  > --SEARCH TABLE model_TSInteraction USING COVERING INDEX index3 (uniqueThreadId=? AND id>?)

  You can see the difference in the actual query bytecode by running:
  "EXPLAIN [query]" (not "EXPLAIN QUERY PLAN"). Notice that when run
  against the first index, it needs to perform two reads. One against
  the table, and another against the index. The only reason it's
  fetching the model is to grab the uniqueId in operation 9.
  > 0     Init           0     12    0                    0   Start at 12
  > 1     OpenRead       0     10    0     67             0   root=10 iDb=0; model_TSInteraction
  > 2     OpenRead       1     162160  0     k(3,,,)        0   root=162160 iDb=0; index1
  > 3     String8        0     1     0     abc123         0   r[1]='abc123'
  > 4     Integer        100   2     0                    0   r[2]=100
  > 5     SeekGT         1     11    1     2              0   key=r[1..2]
  > 6       IdxGT          1     11    1     1              0   key=r[1]
  > 7       DeferredSeek   1     0     0                    0   Move 0 to 1.rowid if needed
  > 8       Column         0     2     3                    0   r[3]=model_TSInteraction.uniqueId
  > 9       ResultRow      3     1     0                    0   output=r[3]
  > 10    Next           1     6     0                    0
  > 11    Halt           0     0     0                    0
  > 12    Transaction    0     0     198   0              1   usesStmtJournal=0
  > 13    Goto           0     1     0                    0

3. SQLite will not consider a partial index covering unless the columns
   defining the index condition are also included in the index content.
   No idea why, but this is why index2 above isn't considered covering
   while index3 is.

4. Index column ordering matters. Briefly, if a WHERE clause in a query
   has any sort of inequality comparisons (everything but IS, IN and =),
   then every subsequent column in an index cannot be used. (mostly)

  For example:
  > CREATE INDEX ON Table(a,b,c);
  > SELECT * WHERE AND b > 100 AND c = "hi";

  Column C isn't going to be usable on the index since we're performing
  an inequality comparison on B. There are loads of exceptions SQLite
  can try to work around this. You can learn more here:
  https://www.sqlite.org/optoverview.html

  How this applies to this change. Consider these two indices:
  > CREATE INDEX index1 ON table(uniqueThreadId, id, recordType, uniqueId) WHERE recordType IS NOT 70;
  > CREATE INDEX index2 ON table(uniqueThreadId, recordType, id, uniqueId) WHERE recordType IS NOT 70;
  > SELECT uniqueId FROM table WHERE uniqueThreadId = 'abc123'
  > AND id > 100 AND recordType IS NOT 70;

  index1 is absolutely going to be the better choice here. Since index1
  is sorted by id first, it's easy for SQLite to binary search to the
  correct offset in the index and then just scan down the index.
  (verifying that each recordType IS NOT 70 along the way, which will
   always hold true because of the partial index condition).

5. Index size matters. At this point, we've constructed this index:
   > CREATE INDEX index1 ON table(uniqueThreadId, id, recordType, uniqueId) WHERE recordType IS NOT 70;

  And SQLite is using it for queries like this. Great!
  > SELECT uniqueId FROM model_TSInteraction
  > WHERE uniqueThreadId = 'whatever' AND recordType IS NOT 70
  > ORDER BY id LIMIT 2 OFFSET 0
  > `--SEARCH TABLE model_TSInteraction USING COVERING INDEX index1 (uniqueThreadId=?)

  Here's a simpler query that we need to run to. It is even less
  specific than the one above so we should expect it to use our
  index.
  > SELECT COUNT(*) FROM model_TSInteraction
  > WHERE uniqueThreadId = 'blah' AND recordType IS NOT 70;
  > `--SEARCH TABLE model_TSInteraction USING COVERING INDEX index_model_TSInteraction_on_uniqueThreadId_and_hasEnded_and_recordType (uniqueThreadId=?)

  As best I can tell, the reason SQLite is picking this existing index
  is because each row in the index is smaller.
  Each row in our index: [String, Int, Int, String]
  Each row in the chosen index: [String, Bool, Int]

  The query planner had to choose, was it better off picking the index
  that's more specific but larger, or less specific but smaller. It
  picked the latter.

  To work around this we can add a *second* index, that's even smaller
  that the index it's choosing.
  > CREATE INDEX index2 ON table(uniqueThreadId, recordType) WHERE recordType IS NOT 70;

  Now the query picks our new index to run the count.
  --SEARCH TABLE model_TSInteraction USING COVERING INDEX index2 (uniqueThreadId=?)
2021-08-30 08:55:53 -03:00
Michelle Linington
c37312478d More index tuning 2021-08-26 20:57:06 -07:00
Michelle Linington
d69483e489 Update index to use expression instead of column 2021-08-26 20:57:06 -07:00
Michelle Linington
c7444cb01d Remove isHidden column, use recordType instead 2021-08-26 20:57:06 -07:00
Michelle Linington
5b1b0d3bd0 Make hiddenUntilTimestamp a boolean column 2021-08-26 20:57:06 -07:00
Michelle Linington
ee85ecc965 Migrations, renames, more aggressive scheduling of cleanup 2021-08-26 20:29:21 -07:00