_MsgParseExpectation((_MsgTok.NICK_USER_HOST, ':parter'),
                          'PART',
                          ((_MsgTok.CHANNEL, ':channel'),)),
-    _MsgParseExpectation((_MsgTok.NICK_USER_HOST, ':parter'),
-                         'PART',
-                         ((_MsgTok.CHANNEL, ':channel'),
-                          (_MsgTok.ANY, ':reason'))),
 ]
 
 # messaging
                     self._caps.end_negotiation()
         elif ret['verb'] == 'ERROR':
             self.close()
-        elif ret['verb'] == 'JOIN':
-            self._log(f'{ret["joiner"]} {msg.verb.lower()}s '
-                      + f'{ret["channel"]["id"]}',
-                      scope=LogScope.CHAT, target=ret['channel']['id'])
-            if ret['joiner'].nick != self._db.nickname:
-                ret['channel']['db'].append_completable(
-                        'users', ret['joiner'].nick, True)
+        elif ret['verb'] == 'JOIN' and ret['joiner'].nick != self._db.nickname:
+            ret['channel']['db'].append_completable(
+                    'users', ret['joiner'].nick, True)
         elif ret['verb'] == 'NICK':
             if ret['named'].nick == self._db.nickname:
                 self._db.nickname = ret['nickname']
                                   else ret['channel']['id'])}
             self._log(ret['message'], out=False, **kw)
         elif ret['verb'] == 'PART':
-            reason = f': {ret["reason"]}' if 'reason' in ret else ''
-            self._log(f'{ret["parter"]} {msg.verb.lower()}s '
-                      + f'{ret["channel"]["id"]}{reason}',
-                      scope=LogScope.CHAT, target=ret['channel']['id'])
             if ret['parter'].nick == self._db.nickname:
                 self._db.del_chan(ret['channel']['id'])
             else:
 
         d[update.arg] = update.value
         return update.value != old_value
 
-    def set_and_check_for_change(self, update: _Update) -> bool:
+    def set_and_check_for_change(self, update: _Update
+                                 ) -> bool | dict[str, tuple[str, ...]]:
         'Apply update, return if that actually made a difference.'
         self._typecheck(update.path, update.value)
         old_value = getattr(self, update.path)
 class _ChannelDb(_Db):
     users: tuple[str, ...]
 
-    def set_and_check_for_change(self, update: _Update) -> bool:
+    def set_and_check_for_change(self, update: _Update
+                                 ) -> bool | dict[str, tuple[str, ...]]:
         if isinstance(getattr(self, update.path), dict):
             return self._set_and_check_for_dict(update)
+        if update.path == 'users':
+            assert isinstance(update.value, tuple)
+            d = {'joins': tuple(user for user in update.value
+                                if user not in self.users),
+                 'parts': tuple(user for user in self.users
+                                if user not in update.value)}
+            return d if super().set_and_check_for_change(update) else False
         return super().set_and_check_for_change(update)
 
 
     motd: tuple[str]
     _channels: dict[str, _ChannelDb]
 
-    def set_and_check_for_change(self, update: _Update) -> bool:
+    def set_and_check_for_change(self, update: _Update
+                                 ) -> bool | dict[str, tuple[str, ...]]:
         if update.is_chan:
             chan_name = update.path
             if update.value is None and not update.arg:
         verb = 'cleared' if update.value is None else 'changed to:'
         what = f'{update.path}:{update.arg}' if update.arg else update.path
         log_kwargs = {'target': update.path} if update.is_chan else {}
-        if not self.db.set_and_check_for_change(update):
+        result = self.db.set_and_check_for_change(update)
+        if result is False:
             return False
-        announcement = f'{what} {verb}'
-        if isinstance(update.value, tuple) or announcement[-1] != ':':
-            self.log(announcement, scope=scope, **log_kwargs)
-        if isinstance(update.value, tuple):
-            for item in update.value:
-                self.log(f'  {item}', scope=scope, **log_kwargs)
-        elif announcement[-1] == ':':
-            self.log(f'{announcement} [{update.display}]',
-                     scope=scope, **log_kwargs)
+        if isinstance(result, dict):
+            for verb, names in result.items():
+                for name in names:
+                    self.log(f'{name} {verb}', scope=scope, **log_kwargs)
+        else:
+            announcement = f'{what} {verb}'
+            if isinstance(update.value, tuple) or announcement[-1] != ':':
+                self.log(announcement, scope=scope, **log_kwargs)
+            if isinstance(update.value, tuple):
+                for item in update.value:
+                    self.log(f'  {item}', scope=scope, **log_kwargs)
+            elif announcement[-1] == ':':
+                self.log(f'{announcement} [{update.display}]',
+                         scope=scope, **log_kwargs)
         for win in [w for w in self.windows if isinstance(w, _ChatWindow)]:
             win.set_prompt_prefix()
         return bool([w for w in self.windows if w.tainted])