prompt: You will be provided with a partial code base and an issue statement explaining a problem to resolve.
  <issue>
  Update first message ID when first message is deleted
  When a message is deleted, we should update the stored ID of the first message in the stream. Because we currently do not, deleting the first message may result in an extraneous "more topics" link in the left sidebar, with no additional topics shown when you click it.
  
  Note: The symptom may be hard to replicate; we should focus on fixing the technical issue, as described in @timabbott 's comment below.
  
  </issue>
  <code>
  [start of zerver/actions/message_delete.py]
  1 from typing import Iterable, List, TypedDict
  2 
  3 from zerver.lib import retention
  4 from zerver.lib.retention import move_messages_to_archive
  5 from zerver.lib.stream_subscription import get_active_subscriptions_for_stream_id
  6 from zerver.models import Message, Realm, UserMessage, UserProfile
  7 from zerver.tornado.django_api import send_event_on_commit
  8 
  9 
  10 class DeleteMessagesEvent(TypedDict, total=False):
  11     type: str
  12     message_ids: List[int]
  13     message_type: str
  14     topic: str
  15     stream_id: int
  16 
  17 
  18 def do_delete_messages(realm: Realm, messages: Iterable[Message]) -> None:
  19     # messages in delete_message event belong to the same topic
  20     # or is a single direct message, as any other behaviour is not possible with
  21     # the current callers to this method.
  22     messages = list(messages)
  23     message_ids = [message.id for message in messages]
  24     if not message_ids:
  25         return
  26 
  27     event: DeleteMessagesEvent = {
  28         "type": "delete_message",
  29         "message_ids": message_ids,
  30     }
  31 
  32     sample_message = messages[0]
  33     message_type = "stream"
  34     users_to_notify = []
  35     if not sample_message.is_stream_message():
  36         assert len(messages) == 1
  37         message_type = "private"
  38         ums = UserMessage.objects.filter(message_id__in=message_ids)
  39         users_to_notify = [um.user_profile_id for um in ums]
  40         archiving_chunk_size = retention.MESSAGE_BATCH_SIZE
  41 
  42     if message_type == "stream":
  43         stream_id = sample_message.recipient.type_id
  44         event["stream_id"] = stream_id
  45         event["topic"] = sample_message.topic_name()
  46         subscriptions = get_active_subscriptions_for_stream_id(
  47             stream_id, include_deactivated_users=False
  48         )
  49         # We exclude long-term idle users, since they by definition have no active clients.
  50         subscriptions = subscriptions.exclude(user_profile__long_term_idle=True)
  51         users_to_notify = list(subscriptions.values_list("user_profile_id", flat=True))
  52         archiving_chunk_size = retention.STREAM_MESSAGE_BATCH_SIZE
  53 
  54     move_messages_to_archive(message_ids, realm=realm, chunk_size=archiving_chunk_size)
  55 
  56     event["message_type"] = message_type
  57     send_event_on_commit(realm, event, users_to_notify)
  58 
  59 
  60 def do_delete_messages_by_sender(user: UserProfile) -> None:
  61     message_ids = list(
  62         # Uses index: zerver_message_realm_sender_recipient (prefix)
  63         Message.objects.filter(realm_id=user.realm_id, sender=user)
  64         .values_list("id", flat=True)
  65         .order_by("id")
  66     )
  67     if message_ids:
  68         move_messages_to_archive(message_ids, chunk_size=retention.STREAM_MESSAGE_BATCH_SIZE)
  69
  [end of zerver/actions/message_delete.py]
  [start of version.py]
  1 import os
  2 
  3 ZULIP_VERSION = "9.0-dev+git"
  4 
  5 # Add information on number of commits and commit hash to version, if available
  6 zulip_git_version_file = os.path.join(
  7     os.path.dirname(os.path.abspath(__file__)), "zulip-git-version"
  8 )
  9 lines = [ZULIP_VERSION, ""]
  10 if os.path.exists(zulip_git_version_file):
  11     with open(zulip_git_version_file) as f:
  12         lines = [*f, "", ""]
  13 ZULIP_VERSION = lines.pop(0).strip()
  14 ZULIP_MERGE_BASE = lines.pop(0).strip()
  15 
  16 LATEST_MAJOR_VERSION = "8.0"
  17 LATEST_RELEASE_VERSION = "8.3"
  18 LATEST_RELEASE_ANNOUNCEMENT = "https://blog.zulip.com/2023/12/15/zulip-8-0-released/"
  19 
  20 # Versions of the desktop app below DESKTOP_MINIMUM_VERSION will be
  21 # prevented from connecting to the Zulip server.  Versions above
  22 # DESKTOP_MINIMUM_VERSION but below DESKTOP_WARNING_VERSION will have
  23 # a banner at the top of the page asking the user to upgrade.
  24 DESKTOP_MINIMUM_VERSION = "5.4.3"
  25 DESKTOP_WARNING_VERSION = "5.9.3"
  26 
  27 # Bump the API_FEATURE_LEVEL whenever an API change is made
  28 # that clients might want to condition on.  If we forget at
  29 # the time we make the change, then bump it later as soon
  30 # as we notice; clients using API_FEATURE_LEVEL will just not
  31 # use the new feature/API until the bump.
  32 #
  33 # Changes should be accompanied by documentation explaining what the
  34 # new level means in api_docs/changelog.md, as well as "**Changes**"
  35 # entries in the endpoint's documentation in `zulip.yaml`.
  36 API_FEATURE_LEVEL = 255
  37 
  38 # Bump the minor PROVISION_VERSION to indicate that folks should provision
  39 # only when going from an old version of the code to a newer version. Bump
  40 # the major version to indicate that folks should provision in both
  41 # directions.
  42 
  43 # Typically,
  44 # * adding a dependency only requires a minor version bump;
  45 # * removing a dependency requires a major version bump;
  46 # * upgrading a dependency requires a major version bump, unless the
  47 #   upgraded dependency is backwards compatible with all of our
  48 #   historical commits sharing the same major version, in which case a
  49 #   minor version bump suffices.
  50 
  51 PROVISION_VERSION = (269, 3)  # last bumped 2024-04-29 for adding pyasyncore
  52
  [end of version.py]
  </code>
  
  I need you to solve this issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the following format.
  <patch>
  diff --git a/file.py b/file.py
  --- a/file.py
  +++ b/file.py
  @@ -1,27 +1,35 @@
   def euclidean(a, b):
  -    while b:
  -        a, b = b, a % b
  -    return a
  +    if b == 0:
  +        return a
  +    return euclidean(b, a % b)
   
   
   def bresenham(x0, y0, x1, y1):
       points = []
       dx = abs(x1 - x0)
       dy = abs(y1 - y0)
  -    sx = 1 if x0 < x1 else -1
  -    sy = 1 if y0 < y1 else -1
  -    err = dx - dy
  +    x, y = x0, y0
  +    sx = -1 if x0 > x1 else 1
  +    sy = -1 if y0 > y1 else 1
   
  -    while True:
  -        points.append((x0, y0))
  -        if x0 == x1 and y0 == y1:
  -            break
  -        e2 = 2 * err
  -        if e2 > -dy:
  -            err -= dy
  -            x0 += sx
  -        if e2 < dx:
  -            err += dx
  -            y0 += sy
  +    if dx > dy:
  +        err = dx / 2.0
  +        while x != x1:
  +            points.append((x, y))
  +            err -= dy
  +            if err < 0:
  +                y += sy
  +                err += dx
  +            x += sx
  +    else:
  +        err = dy / 2.0
  +        while y != y1:
  +            points.append((x, y))
  +            err -= dx
  +            if err < 0:
  +                x += sx
  +                err += dy
  +            y += sy
  +
  +    points.append((x, y))
       return points
  
  </patch>
golden_diff: diff --git a/version.py b/version.py
  --- a/version.py
  +++ b/version.py
  @@ -33,7 +33,7 @@
   # Changes should be accompanied by documentation explaining what the
   # new level means in api_docs/changelog.md, as well as "**Changes**"
   # entries in the endpoint's documentation in `zulip.yaml`.
  -API_FEATURE_LEVEL = 255
  +API_FEATURE_LEVEL = 256
   
   # Bump the minor PROVISION_VERSION to indicate that folks should provision
   # only when going from an old version of the code to a newer version. Bump
  diff --git a/zerver/actions/message_delete.py b/zerver/actions/message_delete.py
  --- a/zerver/actions/message_delete.py
  +++ b/zerver/actions/message_delete.py
  @@ -3,7 +3,7 @@
   from zerver.lib import retention
   from zerver.lib.retention import move_messages_to_archive
   from zerver.lib.stream_subscription import get_active_subscriptions_for_stream_id
  -from zerver.models import Message, Realm, UserMessage, UserProfile
  +from zerver.models import Message, Realm, Stream, UserMessage, UserProfile
   from zerver.tornado.django_api import send_event_on_commit
   
   
  @@ -15,6 +15,34 @@
       stream_id: int
   
   
  +def check_update_first_message_id(
  +    realm: Realm, stream: Stream, message_ids: List[int], users_to_notify: Iterable[int]
  +) -> None:
  +    # This will not update the `first_message_id` of streams where the
  +    # first message was deleted prior to the implementation of this function.
  +    assert stream.recipient_id is not None
  +    if stream.first_message_id not in message_ids:
  +        return
  +    current_first_message_id = (
  +        Message.objects.filter(realm_id=realm.id, recipient_id=stream.recipient_id)
  +        .values_list("id", flat=True)
  +        .order_by("id")
  +        .first()
  +    )
  +
  +    stream.first_message_id = current_first_message_id
  +    stream.save(update_fields=["first_message_id"])
  +
  +    stream_event = dict(
  +        type="stream",
  +        op="update",
  +        property="first_message_id",
  +        value=stream.first_message_id,
  +        stream_id=stream.id,
  +    )
  +    send_event_on_commit(realm, stream_event, users_to_notify)
  +
  +
   def do_delete_messages(realm: Realm, messages: Iterable[Message]) -> None:
       # messages in delete_message event belong to the same topic
       # or is a single direct message, as any other behaviour is not possible with
  @@ -52,6 +80,9 @@
           archiving_chunk_size = retention.STREAM_MESSAGE_BATCH_SIZE
   
       move_messages_to_archive(message_ids, realm=realm, chunk_size=archiving_chunk_size)
  +    if message_type == "stream":
  +        stream = Stream.objects.get(id=sample_message.recipient.type_id)
  +        check_update_first_message_id(realm, stream, message_ids, users_to_notify)
   
       event["message_type"] = message_type
       send_event_on_commit(realm, event, users_to_notify)
  
verification_info: {"golden_diff": "diff --git a/version.py b/version.py\n--- a/version.py\n+++ b/version.py\n@@ -33,7 +33,7 @@\n # Changes should be accompanied by documentation explaining what the\n # new level means in api_docs/changelog.md, as well as \"**Changes**\"\n # entries in the endpoint's documentation in `zulip.yaml`.\n-API_FEATURE_LEVEL = 255\n+API_FEATURE_LEVEL = 256\n \n # Bump the minor PROVISION_VERSION to indicate that folks should provision\n # only when going from an old version of the code to a newer version. Bump\ndiff --git a/zerver/actions/message_delete.py b/zerver/actions/message_delete.py\n--- a/zerver/actions/message_delete.py\n+++ b/zerver/actions/message_delete.py\n@@ -3,7 +3,7 @@\n from zerver.lib import retention\n from zerver.lib.retention import move_messages_to_archive\n from zerver.lib.stream_subscription import get_active_subscriptions_for_stream_id\n-from zerver.models import Message, Realm, UserMessage, UserProfile\n+from zerver.models import Message, Realm, Stream, UserMessage, UserProfile\n from zerver.tornado.django_api import send_event_on_commit\n \n \n@@ -15,6 +15,34 @@\n     stream_id: int\n \n \n+def check_update_first_message_id(\n+    realm: Realm, stream: Stream, message_ids: List[int], users_to_notify: Iterable[int]\n+) -> None:\n+    # This will not update the `first_message_id` of streams where the\n+    # first message was deleted prior to the implementation of this function.\n+    assert stream.recipient_id is not None\n+    if stream.first_message_id not in message_ids:\n+        return\n+    current_first_message_id = (\n+        Message.objects.filter(realm_id=realm.id, recipient_id=stream.recipient_id)\n+        .values_list(\"id\", flat=True)\n+        .order_by(\"id\")\n+        .first()\n+    )\n+\n+    stream.first_message_id = current_first_message_id\n+    stream.save(update_fields=[\"first_message_id\"])\n+\n+    stream_event = dict(\n+        type=\"stream\",\n+        op=\"update\",\n+        property=\"first_message_id\",\n+        value=stream.first_message_id,\n+        stream_id=stream.id,\n+    )\n+    send_event_on_commit(realm, stream_event, users_to_notify)\n+\n+\n def do_delete_messages(realm: Realm, messages: Iterable[Message]) -> None:\n     # messages in delete_message event belong to the same topic\n     # or is a single direct message, as any other behaviour is not possible with\n@@ -52,6 +80,9 @@\n         archiving_chunk_size = retention.STREAM_MESSAGE_BATCH_SIZE\n \n     move_messages_to_archive(message_ids, realm=realm, chunk_size=archiving_chunk_size)\n+    if message_type == \"stream\":\n+        stream = Stream.objects.get(id=sample_message.recipient.type_id)\n+        check_update_first_message_id(realm, stream, message_ids, users_to_notify)\n \n     event[\"message_type\"] = message_type\n     send_event_on_commit(realm, event, users_to_notify)\n", "issue": "Update first message ID when first message is deleted\nWhen a message is deleted, we should update the stored ID of the first message in the stream. Because we currently do not, deleting the first message may result in an extraneous \"more topics\" link in the left sidebar, with no additional topics shown when you click it.\r\n\r\nNote: The symptom may be hard to replicate; we should focus on fixing the technical issue, as described in @timabbott 's comment below.\n", "before_files": [{"content": "from typing import Iterable, List, TypedDict\n\nfrom zerver.lib import retention\nfrom zerver.lib.retention import move_messages_to_archive\nfrom zerver.lib.stream_subscription import get_active_subscriptions_for_stream_id\nfrom zerver.models import Message, Realm, UserMessage, UserProfile\nfrom zerver.tornado.django_api import send_event_on_commit\n\n\nclass DeleteMessagesEvent(TypedDict, total=False):\n    type: str\n    message_ids: List[int]\n    message_type: str\n    topic: str\n    stream_id: int\n\n\ndef do_delete_messages(realm: Realm, messages: Iterable[Message]) -> None:\n    # messages in delete_message event belong to the same topic\n    # or is a single direct message, as any other behaviour is not possible with\n    # the current callers to this method.\n    messages = list(messages)\n    message_ids = [message.id for message in messages]\n    if not message_ids:\n        return\n\n    event: DeleteMessagesEvent = {\n        \"type\": \"delete_message\",\n        \"message_ids\": message_ids,\n    }\n\n    sample_message = messages[0]\n    message_type = \"stream\"\n    users_to_notify = []\n    if not sample_message.is_stream_message():\n        assert len(messages) == 1\n        message_type = \"private\"\n        ums = UserMessage.objects.filter(message_id__in=message_ids)\n        users_to_notify = [um.user_profile_id for um in ums]\n        archiving_chunk_size = retention.MESSAGE_BATCH_SIZE\n\n    if message_type == \"stream\":\n        stream_id = sample_message.recipient.type_id\n        event[\"stream_id\"] = stream_id\n        event[\"topic\"] = sample_message.topic_name()\n        subscriptions = get_active_subscriptions_for_stream_id(\n            stream_id, include_deactivated_users=False\n        )\n        # We exclude long-term idle users, since they by definition have no active clients.\n        subscriptions = subscriptions.exclude(user_profile__long_term_idle=True)\n        users_to_notify = list(subscriptions.values_list(\"user_profile_id\", flat=True))\n        archiving_chunk_size = retention.STREAM_MESSAGE_BATCH_SIZE\n\n    move_messages_to_archive(message_ids, realm=realm, chunk_size=archiving_chunk_size)\n\n    event[\"message_type\"] = message_type\n    send_event_on_commit(realm, event, users_to_notify)\n\n\ndef do_delete_messages_by_sender(user: UserProfile) -> None:\n    message_ids = list(\n        # Uses index: zerver_message_realm_sender_recipient (prefix)\n        Message.objects.filter(realm_id=user.realm_id, sender=user)\n        .values_list(\"id\", flat=True)\n        .order_by(\"id\")\n    )\n    if message_ids:\n        move_messages_to_archive(message_ids, chunk_size=retention.STREAM_MESSAGE_BATCH_SIZE)\n", "path": "zerver/actions/message_delete.py"}, {"content": "import os\n\nZULIP_VERSION = \"9.0-dev+git\"\n\n# Add information on number of commits and commit hash to version, if available\nzulip_git_version_file = os.path.join(\n    os.path.dirname(os.path.abspath(__file__)), \"zulip-git-version\"\n)\nlines = [ZULIP_VERSION, \"\"]\nif os.path.exists(zulip_git_version_file):\n    with open(zulip_git_version_file) as f:\n        lines = [*f, \"\", \"\"]\nZULIP_VERSION = lines.pop(0).strip()\nZULIP_MERGE_BASE = lines.pop(0).strip()\n\nLATEST_MAJOR_VERSION = \"8.0\"\nLATEST_RELEASE_VERSION = \"8.3\"\nLATEST_RELEASE_ANNOUNCEMENT = \"https://blog.zulip.com/2023/12/15/zulip-8-0-released/\"\n\n# Versions of the desktop app below DESKTOP_MINIMUM_VERSION will be\n# prevented from connecting to the Zulip server.  Versions above\n# DESKTOP_MINIMUM_VERSION but below DESKTOP_WARNING_VERSION will have\n# a banner at the top of the page asking the user to upgrade.\nDESKTOP_MINIMUM_VERSION = \"5.4.3\"\nDESKTOP_WARNING_VERSION = \"5.9.3\"\n\n# Bump the API_FEATURE_LEVEL whenever an API change is made\n# that clients might want to condition on.  If we forget at\n# the time we make the change, then bump it later as soon\n# as we notice; clients using API_FEATURE_LEVEL will just not\n# use the new feature/API until the bump.\n#\n# Changes should be accompanied by documentation explaining what the\n# new level means in api_docs/changelog.md, as well as \"**Changes**\"\n# entries in the endpoint's documentation in `zulip.yaml`.\nAPI_FEATURE_LEVEL = 255\n\n# Bump the minor PROVISION_VERSION to indicate that folks should provision\n# only when going from an old version of the code to a newer version. Bump\n# the major version to indicate that folks should provision in both\n# directions.\n\n# Typically,\n# * adding a dependency only requires a minor version bump;\n# * removing a dependency requires a major version bump;\n# * upgrading a dependency requires a major version bump, unless the\n#   upgraded dependency is backwards compatible with all of our\n#   historical commits sharing the same major version, in which case a\n#   minor version bump suffices.\n\nPROVISION_VERSION = (269, 3)  # last bumped 2024-04-29 for adding pyasyncore\n", "path": "version.py"}]}