Daniel Brötzmann pushed to branch master at gajim / gajim

Commits:

6 changed files:

Changes:

  • gajim/common/storage/archive.py
    ... ... @@ -134,6 +134,15 @@ class SearchLogRow(NamedTuple):
    134 134
         log_line_id: int
    
    135 135
     
    
    136 136
     
    
    137
    +class HistoryDayRow(NamedTuple):
    
    138
    +    day: int
    
    139
    +
    
    140
    +
    
    141
    +class MessageMetaRow(NamedTuple):
    
    142
    +    log_line_id: int
    
    143
    +    time: float
    
    144
    +
    
    145
    +
    
    137 146
     class LastArchiveMessageRow(NamedTuple):
    
    138 147
         id: int
    
    139 148
         last_mam_id: str
    
    ... ... @@ -551,46 +560,6 @@ def get_conversation_between(self,
    551 560
                 sql,
    
    552 561
                 tuple(jids) + (before, after)).fetchall()
    
    553 562
     
    
    554
    -    @timeit
    
    555
    -    def get_messages_for_date(self,
    
    556
    -                              account: str,
    
    557
    -                              jid: str,
    
    558
    -                              date: datetime.datetime):
    
    559
    -        """
    
    560
    -        Load the complete conversation with a given jid on a specific date
    
    561
    -
    
    562
    -        :param account: The account
    
    563
    -
    
    564
    -        :param jid:     The jid for which we request the conversation
    
    565
    -
    
    566
    -        :param date:    datetime.datetime instance
    
    567
    -                        example: datetime.datetime(year, month, day)
    
    568
    -
    
    569
    -        returns a list of namedtuples
    
    570
    -        """
    
    571
    -
    
    572
    -        jids = [jid]
    
    573
    -        account_id = self.get_account_id(account)
    
    574
    -
    
    575
    -        delta = datetime.timedelta(
    
    576
    -            hours=23, minutes=59, seconds=59, microseconds=999999)
    
    577
    -        date_ts = date.timestamp()
    
    578
    -        delta_ts = (date + delta).timestamp()
    
    579
    -
    
    580
    -        sql = '''
    
    581
    -            SELECT contact_name, time, kind, show, message, subject,
    
    582
    -                   additional_data, log_line_id
    
    583
    -            FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
    
    584
    -            AND account_id = {account_id}
    
    585
    -            AND time BETWEEN ? AND ?
    
    586
    -            ORDER BY time DESC, log_line_id DESC
    
    587
    -            '''.format(jids=', '.join('?' * len(jids)),
    
    588
    -                       account_id=account_id)
    
    589
    -
    
    590
    -        return self._con.execute(
    
    591
    -            sql,
    
    592
    -            tuple(jids) + (date_ts, delta_ts)).fetchall()
    
    593
    -
    
    594 563
         @timeit
    
    595 564
         def search_log(self,
    
    596 565
                        _account: str,
    
    ... ... @@ -751,27 +720,17 @@ def search_all_logs(self,
    751 720
                     yield result
    
    752 721
     
    
    753 722
         @timeit
    
    754
    -    def get_days_with_logs(self,
    
    755
    -                           _account: str,
    
    756
    -                           jid: str,
    
    757
    -                           year: int,
    
    758
    -                           month: int) -> Any:
    
    723
    +    def get_days_with_history(self,
    
    724
    +                              account: str,
    
    725
    +                              jid: str,
    
    726
    +                              year: int,
    
    727
    +                              month: int
    
    728
    +                              ) -> list[HistoryDayRow]:
    
    759 729
             """
    
    760
    -        Request the days in a month where we received messages
    
    761
    -        for a given `jid`.
    
    762
    -
    
    763
    -        :param account: The account
    
    764
    -
    
    765
    -        :param jid:     The jid for which we request the days
    
    766
    -
    
    767
    -        :param year:    The year
    
    768
    -
    
    769
    -        :param month:   The month
    
    770
    -
    
    771
    -        returns a list of namedtuples
    
    730
    +        Get days in month where messages for 'jid' exist
    
    772 731
             """
    
    773 732
             jids = [jid]
    
    774
    -
    
    733
    +        account_id = self.get_account_id(account)
    
    775 734
             kinds = map(str, [KindConstant.STATUS,
    
    776 735
                               KindConstant.GCSTATUS])
    
    777 736
     
    
    ... ... @@ -785,10 +744,12 @@ def get_days_with_logs(self,
    785 744
                 SELECT DISTINCT
    
    786 745
                 CAST(strftime('%d', time, 'unixepoch', 'localtime') AS INTEGER)
    
    787 746
                 AS day FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
    
    747
    +            AND account_id = {account_id}
    
    788 748
                 AND time BETWEEN ? AND ?
    
    789 749
                 AND kind NOT IN ({kinds})
    
    790 750
                 ORDER BY time
    
    791 751
                 """.format(jids=', '.join('?' * len(jids)),
    
    752
    +                       account_id=account_id,
    
    792 753
                            kinds=', '.join(kinds))
    
    793 754
     
    
    794 755
             return self._con.execute(sql, tuple(jids) +
    
    ... ... @@ -796,26 +757,25 @@ def get_days_with_logs(self,
    796 757
                                           (date + delta).timestamp())).fetchall()
    
    797 758
     
    
    798 759
         @timeit
    
    799
    -    def get_last_date_that_has_logs(self, _account: str, jid: str) -> Any:
    
    760
    +    def get_last_history_timestamp(self,
    
    761
    +                                   account: str,
    
    762
    +                                   jid: str
    
    763
    +                                   ) -> Optional[float]:
    
    800 764
             """
    
    801
    -        Get the timestamp of the last message we received for the jid.
    
    802
    -
    
    803
    -        :param account: The account
    
    804
    -
    
    805
    -        :param jid:     The jid for which we request the last timestamp
    
    806
    -
    
    807
    -        returns a timestamp or None
    
    765
    +        Get the timestamp of the last message we received for the jid
    
    808 766
             """
    
    809 767
             jids = [jid]
    
    810
    -
    
    768
    +        account_id = self.get_account_id(account)
    
    811 769
             kinds = map(str, [KindConstant.STATUS,
    
    812 770
                               KindConstant.GCSTATUS])
    
    813 771
     
    
    814 772
             sql = '''
    
    815 773
                 SELECT MAX(time) as time FROM logs
    
    816 774
                 NATURAL JOIN jids WHERE jid IN ({jids})
    
    775
    +            AND account_id = {account_id}
    
    817 776
                 AND kind NOT IN ({kinds})
    
    818
    -            '''.format(jids=', '.join('?' * len(jids)),
    
    777
    +            '''.format(account_id=account_id,
    
    778
    +                       jids=', '.join('?' * len(jids)),
    
    819 779
                            kinds=', '.join(kinds))
    
    820 780
     
    
    821 781
             # fetchone() returns always at least one Row with all
    
    ... ... @@ -823,26 +783,25 @@ def get_last_date_that_has_logs(self, _account: str, jid: str) -> Any:
    823 783
             return self._con.execute(sql, tuple(jids)).fetchone().time
    
    824 784
     
    
    825 785
         @timeit
    
    826
    -    def get_first_date_that_has_logs(self, _account: str, jid: str) -> Any:
    
    786
    +    def get_first_history_timestamp(self,
    
    787
    +                                    account: str,
    
    788
    +                                    jid: str
    
    789
    +                                    ) -> Optional[float]:
    
    827 790
             """
    
    828
    -        Get the timestamp of the first message we received for the jid.
    
    829
    -
    
    830
    -        :param account: The account
    
    831
    -
    
    832
    -        :param jid:     The jid for which we request the first timestamp
    
    833
    -
    
    834
    -        returns a timestamp or None
    
    791
    +        Get the timestamp of the first message we received for the jid
    
    835 792
             """
    
    836 793
             jids = [jid]
    
    837
    -
    
    794
    +        account_id = self.get_account_id(account)
    
    838 795
             kinds = map(str, [KindConstant.STATUS,
    
    839 796
                               KindConstant.GCSTATUS])
    
    840 797
     
    
    841 798
             sql = '''
    
    842 799
                 SELECT MIN(time) as time FROM logs
    
    843 800
                 NATURAL JOIN jids WHERE jid IN ({jids})
    
    801
    +            AND account_id = {account_id}
    
    844 802
                 AND kind NOT IN ({kinds})
    
    845 803
                 '''.format(jids=', '.join('?' * len(jids)),
    
    804
    +                       account_id=account_id,
    
    846 805
                            kinds=', '.join(kinds))
    
    847 806
     
    
    848 807
             # fetchone() returns always at least one Row with all
    
    ... ... @@ -850,25 +809,17 @@ def get_first_date_that_has_logs(self, _account: str, jid: str) -> Any:
    850 809
             return self._con.execute(sql, tuple(jids)).fetchone().time
    
    851 810
     
    
    852 811
         @timeit
    
    853
    -    def get_date_has_logs(self,
    
    854
    -                          _account: str,
    
    855
    -                          jid: str,
    
    856
    -                          date: datetime.datetime) -> Any:
    
    812
    +    def date_has_history(self,
    
    813
    +                         account: str,
    
    814
    +                         jid: str,
    
    815
    +                         date: datetime.datetime
    
    816
    +                         ) -> Optional[float]:
    
    857 817
             """
    
    858
    -        Get single timestamp of a message we received for the jid
    
    859
    -        in the time range of one day.
    
    860
    -
    
    861
    -        :param account: The account
    
    862
    -
    
    863
    -        :param jid:     The jid for which we request the first timestamp
    
    864
    -
    
    865
    -        :param date:    datetime.datetime instance
    
    866
    -                        example: datetime.datetime(year, month, day)
    
    867
    -
    
    868
    -        returns a timestamp or None
    
    818
    +        Get a single timestamp of a message for 'jid'
    
    819
    +        in time range of one day
    
    869 820
             """
    
    870 821
             jids = [jid]
    
    871
    -
    
    822
    +        account_id = self.get_account_id(account)
    
    872 823
             delta = datetime.timedelta(
    
    873 824
                 hours=23, minutes=59, seconds=59, microseconds=999999)
    
    874 825
     
    
    ... ... @@ -878,12 +829,44 @@ def get_date_has_logs(self,
    878 829
             sql = '''
    
    879 830
                 SELECT time
    
    880 831
                 FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
    
    832
    +            AND account_id = {account_id}
    
    881 833
                 AND time BETWEEN ? AND ?
    
    882
    -            '''.format(jids=', '.join('?' * len(jids)))
    
    834
    +            '''.format(jids=', '.join('?' * len(jids)),
    
    835
    +                       account_id=account_id)
    
    883 836
     
    
    884 837
             return self._con.execute(
    
    885 838
                 sql, tuple(jids) + (start, end)).fetchone()
    
    886 839
     
    
    840
    +    @timeit
    
    841
    +    def get_first_message_meta_for_date(self,
    
    842
    +                                        account: str,
    
    843
    +                                        jid: str,
    
    844
    +                                        date: datetime.datetime
    
    845
    +                                        ) -> Optional[MessageMetaRow]:
    
    846
    +        """
    
    847
    +        Load meta data for the first message of a specific date
    
    848
    +        """
    
    849
    +        jids = [jid]
    
    850
    +        account_id = self.get_account_id(account)
    
    851
    +
    
    852
    +        delta = datetime.timedelta(
    
    853
    +            hours=23, minutes=59, seconds=59, microseconds=999999)
    
    854
    +        date_ts = date.timestamp()
    
    855
    +        delta_ts = (date + delta).timestamp()
    
    856
    +
    
    857
    +        sql = '''
    
    858
    +            SELECT time, log_line_id
    
    859
    +            FROM logs NATURAL JOIN jids WHERE jid IN ({jids})
    
    860
    +            AND account_id = {account_id}
    
    861
    +            AND time BETWEEN ? AND ?
    
    862
    +            ORDER BY time ASC, log_line_id ASC
    
    863
    +            '''.format(jids=', '.join('?' * len(jids)),
    
    864
    +                       account_id=account_id)
    
    865
    +
    
    866
    +        return self._con.execute(
    
    867
    +            sql,
    
    868
    +            tuple(jids) + (date_ts, delta_ts)).fetchone()
    
    869
    +
    
    887 870
         @timeit
    
    888 871
         def deduplicate_muc_message(self,
    
    889 872
                                     account: str,
    

  • gajim/data/gui/search_view.ui
    ... ... @@ -2,121 +2,130 @@
    2 2
     <!-- Generated with glade 3.38.2 -->
    
    3 3
     <interface>
    
    4 4
       <requires lib="gtk+" version="3.24"/>
    
    5
    -  <object class="GtkBox" id="header_box">
    
    6
    -    <property name="visible">True</property>
    
    7
    -    <property name="can-focus">False</property>
    
    8
    -    <property name="hexpand">True</property>
    
    9
    -    <property name="spacing">6</property>
    
    10
    -    <child>
    
    11
    -      <object class="GtkSeparator">
    
    12
    -        <property name="visible">True</property>
    
    13
    -        <property name="can-focus">False</property>
    
    14
    -        <property name="valign">center</property>
    
    15
    -      </object>
    
    16
    -      <packing>
    
    17
    -        <property name="expand">True</property>
    
    18
    -        <property name="fill">True</property>
    
    19
    -        <property name="position">0</property>
    
    20
    -      </packing>
    
    21
    -    </child>
    
    22
    -    <child>
    
    23
    -      <object class="GtkLabel" id="header_name_label">
    
    24
    -        <property name="visible">True</property>
    
    25
    -        <property name="can-focus">False</property>
    
    26
    -        <property name="ellipsize">end</property>
    
    27
    -        <property name="max-width-chars">15</property>
    
    28
    -      </object>
    
    29
    -      <packing>
    
    30
    -        <property name="expand">False</property>
    
    31
    -        <property name="fill">True</property>
    
    32
    -        <property name="position">1</property>
    
    33
    -      </packing>
    
    34
    -    </child>
    
    35
    -    <child>
    
    36
    -      <object class="GtkLabel" id="header_date_label">
    
    37
    -        <property name="visible">True</property>
    
    38
    -        <property name="can-focus">False</property>
    
    39
    -      </object>
    
    40
    -      <packing>
    
    41
    -        <property name="expand">False</property>
    
    42
    -        <property name="fill">True</property>
    
    43
    -        <property name="position">2</property>
    
    44
    -      </packing>
    
    45
    -    </child>
    
    46
    -    <child>
    
    47
    -      <object class="GtkSeparator">
    
    48
    -        <property name="visible">True</property>
    
    49
    -        <property name="can-focus">False</property>
    
    50
    -        <property name="valign">center</property>
    
    51
    -      </object>
    
    52
    -      <packing>
    
    53
    -        <property name="expand">True</property>
    
    54
    -        <property name="fill">True</property>
    
    55
    -        <property name="position">3</property>
    
    56
    -      </packing>
    
    57
    -    </child>
    
    58
    -    <style>
    
    59
    -      <class name="search-view-header"/>
    
    60
    -    </style>
    
    61
    -  </object>
    
    62
    -  <!-- n-columns=3 n-rows=2 -->
    
    63
    -  <object class="GtkGrid" id="result_row_grid">
    
    64
    -    <property name="visible">True</property>
    
    5
    +  <object class="GtkPopover" id="calendar_popover">
    
    65 6
         <property name="can-focus">False</property>
    
    66
    -    <property name="row-spacing">3</property>
    
    67
    -    <property name="column-spacing">12</property>
    
    68
    -    <child>
    
    69
    -      <object class="GtkImage" id="row_avatar">
    
    70
    -        <property name="visible">True</property>
    
    71
    -        <property name="can-focus">False</property>
    
    72
    -        <property name="valign">start</property>
    
    73
    -      </object>
    
    74
    -      <packing>
    
    75
    -        <property name="left-attach">0</property>
    
    76
    -        <property name="top-attach">0</property>
    
    77
    -        <property name="height">2</property>
    
    78
    -      </packing>
    
    79
    -    </child>
    
    80 7
         <child>
    
    81
    -      <object class="GtkLabel" id="row_time_label">
    
    82
    -        <property name="visible">True</property>
    
    83
    -        <property name="can-focus">False</property>
    
    84
    -        <property name="halign">end</property>
    
    85
    -        <property name="single-line-mode">True</property>
    
    86
    -        <style>
    
    87
    -          <class name="dim-label"/>
    
    88
    -        </style>
    
    89
    -      </object>
    
    90
    -      <packing>
    
    91
    -        <property name="left-attach">2</property>
    
    92
    -        <property name="top-attach">0</property>
    
    93
    -      </packing>
    
    94
    -    </child>
    
    95
    -    <child>
    
    96
    -      <object class="GtkLabel" id="row_name_label">
    
    8
    +      <object class="GtkBox">
    
    97 9
             <property name="visible">True</property>
    
    98 10
             <property name="can-focus">False</property>
    
    99
    -        <property name="halign">start</property>
    
    100
    -        <property name="ellipsize">end</property>
    
    101
    -        <property name="single-line-mode">True</property>
    
    11
    +        <property name="orientation">vertical</property>
    
    12
    +        <property name="spacing">6</property>
    
    13
    +        <child>
    
    14
    +          <object class="GtkCalendar" id="calendar">
    
    15
    +            <property name="visible">True</property>
    
    16
    +            <property name="can-focus">True</property>
    
    17
    +            <property name="year">2022</property>
    
    18
    +            <property name="month">4</property>
    
    19
    +            <property name="day">21</property>
    
    20
    +            <signal name="day-selected" handler="_on_date_selected" swapped="no"/>
    
    21
    +            <signal name="month-changed" handler="_on_month_changed" swapped="no"/>
    
    22
    +          </object>
    
    23
    +          <packing>
    
    24
    +            <property name="expand">False</property>
    
    25
    +            <property name="fill">True</property>
    
    26
    +            <property name="position">0</property>
    
    27
    +          </packing>
    
    28
    +        </child>
    
    29
    +        <child>
    
    30
    +          <object class="GtkBox">
    
    31
    +            <property name="visible">True</property>
    
    32
    +            <property name="can-focus">False</property>
    
    33
    +            <property name="homogeneous">True</property>
    
    34
    +            <child>
    
    35
    +              <object class="GtkButton">
    
    36
    +                <property name="visible">True</property>
    
    37
    +                <property name="can-focus">True</property>
    
    38
    +                <property name="receives-default">True</property>
    
    39
    +                <signal name="clicked" handler="_on_first_date_selected" swapped="no"/>
    
    40
    +                <child>
    
    41
    +                  <object class="GtkImage">
    
    42
    +                    <property name="visible">True</property>
    
    43
    +                    <property name="can-focus">False</property>
    
    44
    +                    <property name="icon-name">go-first-symbolic</property>
    
    45
    +                  </object>
    
    46
    +                </child>
    
    47
    +              </object>
    
    48
    +              <packing>
    
    49
    +                <property name="expand">False</property>
    
    50
    +                <property name="fill">True</property>
    
    51
    +                <property name="position">0</property>
    
    52
    +              </packing>
    
    53
    +            </child>
    
    54
    +            <child>
    
    55
    +              <object class="GtkButton">
    
    56
    +                <property name="visible">True</property>
    
    57
    +                <property name="can-focus">True</property>
    
    58
    +                <property name="receives-default">True</property>
    
    59
    +                <signal name="clicked" handler="_on_previous_date_selected" swapped="no"/>
    
    60
    +                <child>
    
    61
    +                  <object class="GtkImage">
    
    62
    +                    <property name="visible">True</property>
    
    63
    +                    <property name="can-focus">False</property>
    
    64
    +                    <property name="icon-name">go-previous-symbolic</property>
    
    65
    +                  </object>
    
    66
    +                </child>
    
    67
    +              </object>
    
    68
    +              <packing>
    
    69
    +                <property name="expand">False</property>
    
    70
    +                <property name="fill">True</property>
    
    71
    +                <property name="position">1</property>
    
    72
    +              </packing>
    
    73
    +            </child>
    
    74
    +            <child>
    
    75
    +              <object class="GtkButton">
    
    76
    +                <property name="visible">True</property>
    
    77
    +                <property name="can-focus">True</property>
    
    78
    +                <property name="receives-default">True</property>
    
    79
    +                <signal name="clicked" handler="_on_next_date_selected" swapped="no"/>
    
    80
    +                <child>
    
    81
    +                  <object class="GtkImage">
    
    82
    +                    <property name="visible">True</property>
    
    83
    +                    <property name="can-focus">False</property>
    
    84
    +                    <property name="icon-name">go-next-symbolic</property>
    
    85
    +                  </object>
    
    86
    +                </child>
    
    87
    +              </object>
    
    88
    +              <packing>
    
    89
    +                <property name="expand">False</property>
    
    90
    +                <property name="fill">True</property>
    
    91
    +                <property name="position">2</property>
    
    92
    +              </packing>
    
    93
    +            </child>
    
    94
    +            <child>
    
    95
    +              <object class="GtkButton">
    
    96
    +                <property name="visible">True</property>
    
    97
    +                <property name="can-focus">True</property>
    
    98
    +                <property name="receives-default">True</property>
    
    99
    +                <signal name="clicked" handler="_on_last_date_selected" swapped="no"/>
    
    100
    +                <child>
    
    101
    +                  <object class="GtkImage">
    
    102
    +                    <property name="visible">True</property>
    
    103
    +                    <property name="can-focus">False</property>
    
    104
    +                    <property name="icon-name">go-last-symbolic</property>
    
    105
    +                  </object>
    
    106
    +                </child>
    
    107
    +              </object>
    
    108
    +              <packing>
    
    109
    +                <property name="expand">False</property>
    
    110
    +                <property name="fill">True</property>
    
    111
    +                <property name="position">3</property>
    
    112
    +              </packing>
    
    113
    +            </child>
    
    114
    +            <style>
    
    115
    +              <class name="linked"/>
    
    116
    +            </style>
    
    117
    +          </object>
    
    118
    +          <packing>
    
    119
    +            <property name="expand">False</property>
    
    120
    +            <property name="fill">True</property>
    
    121
    +            <property name="position">1</property>
    
    122
    +          </packing>
    
    123
    +        </child>
    
    102 124
             <style>
    
    103
    -          <class name="bold"/>
    
    125
    +          <class name="padding-6"/>
    
    104 126
             </style>
    
    105 127
           </object>
    
    106
    -      <packing>
    
    107
    -        <property name="left-attach">1</property>
    
    108
    -        <property name="top-attach">0</property>
    
    109
    -      </packing>
    
    110
    -    </child>
    
    111
    -    <child>
    
    112
    -      <placeholder/>
    
    113
    -    </child>
    
    114
    -    <child>
    
    115
    -      <placeholder/>
    
    116 128
         </child>
    
    117
    -    <style>
    
    118
    -      <class name="search-view-row-grid"/>
    
    119
    -    </style>
    
    120 129
       </object>
    
    121 130
       <object class="GtkBox" id="search_box">
    
    122 131
         <property name="visible">True</property>
    
    ... ... @@ -141,7 +150,8 @@
    141 150
               <packing>
    
    142 151
                 <property name="expand">False</property>
    
    143 152
                 <property name="fill">True</property>
    
    144
    -            <property name="position">2</property>
    
    153
    +            <property name="pack-type">end</property>
    
    154
    +            <property name="position">3</property>
    
    145 155
               </packing>
    
    146 156
             </child>
    
    147 157
             <child>
    
    ... ... @@ -163,7 +173,30 @@
    163 173
               <packing>
    
    164 174
                 <property name="expand">False</property>
    
    165 175
                 <property name="fill">True</property>
    
    166
    -            <property name="position">0</property>
    
    176
    +            <property name="position">1</property>
    
    177
    +          </packing>
    
    178
    +        </child>
    
    179
    +        <child>
    
    180
    +          <object class="GtkMenuButton" id="calendar_button">
    
    181
    +            <property name="visible">True</property>
    
    182
    +            <property name="can-focus">True</property>
    
    183
    +            <property name="focus-on-click">False</property>
    
    184
    +            <property name="receives-default">True</property>
    
    185
    +            <property name="tooltip-text" translatable="yes">Browse history by date</property>
    
    186
    +            <property name="popover">calendar_popover</property>
    
    187
    +            <child>
    
    188
    +              <object class="GtkImage">
    
    189
    +                <property name="visible">True</property>
    
    190
    +                <property name="can-focus">False</property>
    
    191
    +                <property name="icon-name">x-office-calendar-symbolic</property>
    
    192
    +              </object>
    
    193
    +            </child>
    
    194
    +          </object>
    
    195
    +          <packing>
    
    196
    +            <property name="expand">False</property>
    
    197
    +            <property name="fill">True</property>
    
    198
    +            <property name="pack-type">end</property>
    
    199
    +            <property name="position">2</property>
    
    167 200
               </packing>
    
    168 201
             </child>
    
    169 202
           </object>
    
    ... ... @@ -304,4 +337,120 @@
    304 337
           <class name="search-view"/>
    
    305 338
         </style>
    
    306 339
       </object>
    
    340
    +  <object class="GtkBox" id="header_box">
    
    341
    +    <property name="visible">True</property>
    
    342
    +    <property name="can-focus">False</property>
    
    343
    +    <property name="hexpand">True</property>
    
    344
    +    <property name="spacing">6</property>
    
    345
    +    <child>
    
    346
    +      <object class="GtkSeparator">
    
    347
    +        <property name="visible">True</property>
    
    348
    +        <property name="can-focus">False</property>
    
    349
    +        <property name="valign">center</property>
    
    350
    +      </object>
    
    351
    +      <packing>
    
    352
    +        <property name="expand">True</property>
    
    353
    +        <property name="fill">True</property>
    
    354
    +        <property name="position">0</property>
    
    355
    +      </packing>
    
    356
    +    </child>
    
    357
    +    <child>
    
    358
    +      <object class="GtkLabel" id="header_name_label">
    
    359
    +        <property name="visible">True</property>
    
    360
    +        <property name="can-focus">False</property>
    
    361
    +        <property name="ellipsize">end</property>
    
    362
    +        <property name="max-width-chars">15</property>
    
    363
    +      </object>
    
    364
    +      <packing>
    
    365
    +        <property name="expand">False</property>
    
    366
    +        <property name="fill">True</property>
    
    367
    +        <property name="position">1</property>
    
    368
    +      </packing>
    
    369
    +    </child>
    
    370
    +    <child>
    
    371
    +      <object class="GtkLabel" id="header_date_label">
    
    372
    +        <property name="visible">True</property>
    
    373
    +        <property name="can-focus">False</property>
    
    374
    +      </object>
    
    375
    +      <packing>
    
    376
    +        <property name="expand">False</property>
    
    377
    +        <property name="fill">True</property>
    
    378
    +        <property name="position">2</property>
    
    379
    +      </packing>
    
    380
    +    </child>
    
    381
    +    <child>
    
    382
    +      <object class="GtkSeparator">
    
    383
    +        <property name="visible">True</property>
    
    384
    +        <property name="can-focus">False</property>
    
    385
    +        <property name="valign">center</property>
    
    386
    +      </object>
    
    387
    +      <packing>
    
    388
    +        <property name="expand">True</property>
    
    389
    +        <property name="fill">True</property>
    
    390
    +        <property name="position">3</property>
    
    391
    +      </packing>
    
    392
    +    </child>
    
    393
    +    <style>
    
    394
    +      <class name="search-view-header"/>
    
    395
    +    </style>
    
    396
    +  </object>
    
    397
    +  <!-- n-columns=3 n-rows=2 -->
    
    398
    +  <object class="GtkGrid" id="result_row_grid">
    
    399
    +    <property name="visible">True</property>
    
    400
    +    <property name="can-focus">False</property>
    
    401
    +    <property name="row-spacing">3</property>
    
    402
    +    <property name="column-spacing">12</property>
    
    403
    +    <child>
    
    404
    +      <object class="GtkImage" id="row_avatar">
    
    405
    +        <property name="visible">True</property>
    
    406
    +        <property name="can-focus">False</property>
    
    407
    +        <property name="valign">start</property>
    
    408
    +      </object>
    
    409
    +      <packing>
    
    410
    +        <property name="left-attach">0</property>
    
    411
    +        <property name="top-attach">0</property>
    
    412
    +        <property name="height">2</property>
    
    413
    +      </packing>
    
    414
    +    </child>
    
    415
    +    <child>
    
    416
    +      <object class="GtkLabel" id="row_time_label">
    
    417
    +        <property name="visible">True</property>
    
    418
    +        <property name="can-focus">False</property>
    
    419
    +        <property name="halign">end</property>
    
    420
    +        <property name="single-line-mode">True</property>
    
    421
    +        <style>
    
    422
    +          <class name="dim-label"/>
    
    423
    +        </style>
    
    424
    +      </object>
    
    425
    +      <packing>
    
    426
    +        <property name="left-attach">2</property>
    
    427
    +        <property name="top-attach">0</property>
    
    428
    +      </packing>
    
    429
    +    </child>
    
    430
    +    <child>
    
    431
    +      <object class="GtkLabel" id="row_name_label">
    
    432
    +        <property name="visible">True</property>
    
    433
    +        <property name="can-focus">False</property>
    
    434
    +        <property name="halign">start</property>
    
    435
    +        <property name="ellipsize">end</property>
    
    436
    +        <property name="single-line-mode">True</property>
    
    437
    +        <style>
    
    438
    +          <class name="bold"/>
    
    439
    +        </style>
    
    440
    +      </object>
    
    441
    +      <packing>
    
    442
    +        <property name="left-attach">1</property>
    
    443
    +        <property name="top-attach">0</property>
    
    444
    +      </packing>
    
    445
    +    </child>
    
    446
    +    <child>
    
    447
    +      <placeholder/>
    
    448
    +    </child>
    
    449
    +    <child>
    
    450
    +      <placeholder/>
    
    451
    +    </child>
    
    452
    +    <style>
    
    453
    +      <class name="search-view-row-grid"/>
    
    454
    +    </style>
    
    455
    +  </object>
    
    307 456
     </interface>

  • gajim/gtk/builder.pyi
    ... ... @@ -803,6 +803,15 @@ class RosterTooltipBuilder(Builder):
    803 803
     
    
    804 804
     
    
    805 805
     class SearchViewBuilder(Builder):
    
    806
    +    calendar_popover: Gtk.Popover
    
    807
    +    calendar: Gtk.Calendar
    
    808
    +    search_box: Gtk.Box
    
    809
    +    calendar_button: Gtk.MenuButton
    
    810
    +    search_entry: Gtk.SearchEntry
    
    811
    +    search_checkbutton: Gtk.CheckButton
    
    812
    +    date_hint: Gtk.Label
    
    813
    +    results_listbox: Gtk.ListBox
    
    814
    +    placeholder: Gtk.Box
    
    806 815
         header_box: Gtk.Box
    
    807 816
         header_name_label: Gtk.Label
    
    808 817
         header_date_label: Gtk.Label
    
    ... ... @@ -810,12 +819,6 @@ class SearchViewBuilder(Builder):
    810 819
         row_avatar: Gtk.Image
    
    811 820
         row_time_label: Gtk.Label
    
    812 821
         row_name_label: Gtk.Label
    
    813
    -    search_box: Gtk.Box
    
    814
    -    search_entry: Gtk.SearchEntry
    
    815
    -    search_checkbutton: Gtk.CheckButton
    
    816
    -    date_hint: Gtk.Label
    
    817
    -    results_listbox: Gtk.ListBox
    
    818
    -    placeholder: Gtk.Box
    
    819 822
     
    
    820 823
     
    
    821 824
     class ServerInfoBuilder(Builder):
    

  • gajim/gtk/conversation/rows/call.py
    ... ... @@ -80,6 +80,7 @@ def __init__(self,
    80 80
                 sid = db_message.additional_data.get_value('gajim', 'sid')
    
    81 81
                 module = self._client.get_module('Jingle')
    
    82 82
                 self._session = module.get_jingle_session(self._contact.jid, sid)
    
    83
    +            self.log_line_id = db_message.log_line_id
    
    83 84
     
    
    84 85
             self._avatar_placeholder = Gtk.Box()
    
    85 86
             self._avatar_placeholder.set_size_request(AvatarSize.ROSTER, -1)
    

  • gajim/gtk/conversation/rows/file_transfer_jingle.py
    ... ... @@ -82,6 +82,7 @@ def __init__(self,
    82 82
                 self._file_props = FilesProp.getFilePropBySid(sid)
    
    83 83
                 if self._file_props is None:
    
    84 84
                     log.debug('File prop not found for SID: %s', sid)
    
    85
    +            self.log_line_id = db_message.log_line_id
    
    85 86
             else:
    
    86 87
                 assert event is not None
    
    87 88
                 self._file_props = event.file_props
    

  • gajim/gtk/search_view.py
    ... ... @@ -18,7 +18,8 @@
    18 18
     from typing import Iterator
    
    19 19
     from typing import Optional
    
    20 20
     
    
    21
    -import datetime
    
    21
    +from datetime import datetime
    
    22
    +from datetime import timedelta
    
    22 23
     import itertools
    
    23 24
     import logging
    
    24 25
     import time
    
    ... ... @@ -33,11 +34,14 @@
    33 34
     from gajim.common import app
    
    34 35
     from gajim.common import ged
    
    35 36
     from gajim.common.const import AvatarSize
    
    37
    +from gajim.common.const import Direction
    
    36 38
     from gajim.common.const import KindConstant
    
    37 39
     from gajim.common.storage.archive import SearchLogRow
    
    38 40
     
    
    39 41
     from .conversation.message_widget import MessageWidget
    
    40 42
     from .builder import get_builder
    
    43
    +from .util import gtk_month
    
    44
    +from .util import python_month
    
    41 45
     
    
    42 46
     log = logging.getLogger('gajim.gui.search_view')
    
    43 47
     
    
    ... ... @@ -59,6 +63,9 @@ def __init__(self) -> None:
    59 63
             self._results_iterator: Optional[Iterator[SearchLogRow]] = None
    
    60 64
             self._scope: str = 'everywhere'
    
    61 65
     
    
    66
    +        self._first_date: Optional[datetime] = None
    
    67
    +        self._last_date: Optional[datetime] = None
    
    68
    +
    
    62 69
             self._ui = get_builder('search_view.ui')
    
    63 70
             self._ui.results_listbox.set_header_func(self._header_func)
    
    64 71
             self.add(self._ui.search_box)
    
    ... ... @@ -71,6 +78,7 @@ def __init__(self) -> None:
    71 78
             app.ged.register_event_handler('account-disabled',
    
    72 79
                                            ged.GUI1,
    
    73 80
                                            self._on_account_state)
    
    81
    +        self._update_calendar()
    
    74 82
             self.show_all()
    
    75 83
     
    
    76 84
         def _on_account_state(self, _event: Any) -> None:
    
    ... ... @@ -119,7 +127,7 @@ def _on_search(self, entry: Gtk.Entry) -> None:
    119 127
             text, before_filters = self._strip_filters(text, 'before')
    
    120 128
             if before_filters is not None:
    
    121 129
                 try:
    
    122
    -                before_filters = min([datetime.datetime.fromisoformat(date) for
    
    130
    +                before_filters = min([datetime.fromisoformat(date) for
    
    123 131
                                           date in before_filters])
    
    124 132
                 except ValueError:
    
    125 133
                     self._ui.date_hint.show()
    
    ... ... @@ -129,14 +137,14 @@ def _on_search(self, entry: Gtk.Entry) -> None:
    129 137
             text, after_filters = self._strip_filters(text, 'after')
    
    130 138
             if after_filters is not None:
    
    131 139
                 try:
    
    132
    -                after_filters = min([datetime.datetime.fromisoformat(date) for
    
    140
    +                after_filters = min([datetime.fromisoformat(date) for
    
    133 141
                                          date in after_filters])
    
    134 142
                     # if only the day is specified, we want to look after the
    
    135 143
                     # end of that day.
    
    136 144
                     # if precision is increased,we do want to look during the
    
    137 145
                     # day as well.
    
    138 146
                     if after_filters.hour == after_filters.minute == 0:
    
    139
    -                    after_filters += datetime.timedelta(days=1)
    
    147
    +                    after_filters += timedelta(days=1)
    
    140 148
                 except ValueError:
    
    141 149
                     self._ui.date_hint.show()
    
    142 150
                     return
    
    ... ... @@ -202,6 +210,120 @@ def _on_edge_reached(self,
    202 210
     
    
    203 211
             self._add_results()
    
    204 212
     
    
    213
    +    def _update_calendar(self) -> None:
    
    214
    +        if self._scope == 'everywhere':
    
    215
    +            self._ui.calendar_button.set_sensitive(False)
    
    216
    +        else:
    
    217
    +            self._ui.calendar_button.set_sensitive(True)
    
    218
    +
    
    219
    +            first_log = app.storage.archive.get_first_history_timestamp(
    
    220
    +                self._account, self._jid)
    
    221
    +            if first_log is None:
    
    222
    +                return
    
    223
    +            self._first_date = self._get_date_from_timestamp(first_log)
    
    224
    +            last_log = app.storage.archive.get_last_history_timestamp(
    
    225
    +                self._account, self._jid)
    
    226
    +            if last_log is None:
    
    227
    +                return
    
    228
    +            self._last_date = self._get_date_from_timestamp(last_log)
    
    229
    +
    
    230
    +            month = gtk_month(self._last_date.month)
    
    231
    +            self._ui.calendar.select_month(month, self._last_date.year)
    
    232
    +
    
    233
    +    def _on_month_changed(self, calendar: Gtk.Calendar) -> None:
    
    234
    +        # Mark days with history in calendar
    
    235
    +        year, month, _day = calendar.get_date()
    
    236
    +        if year < 1900:
    
    237
    +            calendar.select_month(0, 1900)
    
    238
    +            calendar.select_day(1)
    
    239
    +            return
    
    240
    +
    
    241
    +        calendar.clear_marks()
    
    242
    +        month = python_month(month)
    
    243
    +
    
    244
    +        history_days = app.storage.archive.get_days_with_history(
    
    245
    +            self._account, self._jid, year, month)
    
    246
    +        for date in history_days:
    
    247
    +            calendar.mark_day(date.day)
    
    248
    +
    
    249
    +    def _on_date_selected(self, calendar: Gtk.Calendar) -> None:
    
    250
    +        year, month, day = calendar.get_date()
    
    251
    +        py_m = python_month(month)
    
    252
    +        date = datetime(year, py_m, day)
    
    253
    +        self._scroll_to_date(date)
    
    254
    +
    
    255
    +    def _on_first_date_selected(self, _button: Gtk.Button) -> None:
    
    256
    +        assert self._first_date is not None
    
    257
    +        gtk_m = gtk_month(self._first_date.month)
    
    258
    +        self._ui.calendar.select_month(gtk_m, self._first_date.year)
    
    259
    +        self._ui.calendar.select_day(self._first_date.day)
    
    260
    +
    
    261
    +    def _on_last_date_selected(self, _button: Gtk.Button) -> None:
    
    262
    +        assert self._last_date is not None
    
    263
    +        gtk_m = gtk_month(self._last_date.month)
    
    264
    +        self._ui.calendar.select_month(gtk_m, self._last_date.year)
    
    265
    +        self._ui.calendar.select_day(self._last_date.day)
    
    266
    +
    
    267
    +    def _on_previous_date_selected(self, _button: Gtk.Button) -> None:
    
    268
    +        delta = timedelta(days=-1)
    
    269
    +        assert self._first_date is not None
    
    270
    +        self._select_date(delta, self._first_date, Direction.NEXT)
    
    271
    +
    
    272
    +    def _on_next_date_selected(self, _button: Gtk.Button) -> None:
    
    273
    +        delta = timedelta(days=1)
    
    274
    +        assert self._last_date is not None
    
    275
    +        self._select_date(delta, self._last_date, Direction.PREV)
    
    276
    +
    
    277
    +    def _select_date(self,
    
    278
    +                     delta: timedelta,
    
    279
    +                     end_date: datetime,
    
    280
    +                     direction: Direction
    
    281
    +                     ) -> None:
    
    282
    +        # Iterate through days until history entry found or
    
    283
    +        # supplied end_date (first_date/last_date) reached
    
    284
    +        year, month, day = self._ui.calendar.get_date()
    
    285
    +        py_m = python_month(month)
    
    286
    +        date = datetime(year, py_m, day)
    
    287
    +
    
    288
    +        history = None
    
    289
    +        while history is None:
    
    290
    +            if direction == Direction.PREV:
    
    291
    +                if end_date <= date:
    
    292
    +                    return
    
    293
    +            else:
    
    294
    +                if end_date >= date:
    
    295
    +                    return
    
    296
    +
    
    297
    +            date = date + delta
    
    298
    +            if date == end_date:
    
    299
    +                break
    
    300
    +            history = app.storage.archive.date_has_history(
    
    301
    +                self._account, self._jid, date)
    
    302
    +
    
    303
    +        gtk_m = gtk_month(date.month)
    
    304
    +        self._ui.calendar.select_month(gtk_m, date.year)
    
    305
    +        self._ui.calendar.select_day(date.day)
    
    306
    +
    
    307
    +    def _scroll_to_date(self, date: datetime) -> None:
    
    308
    +        control = app.window.get_active_control()
    
    309
    +        if control is None:
    
    310
    +            return
    
    311
    +        if control.contact.jid == self._jid:
    
    312
    +            meta_row = app.storage.archive.get_first_message_meta_for_date(
    
    313
    +                self._account, self._jid, date)
    
    314
    +            if meta_row is None:
    
    315
    +                return
    
    316
    +            control.scroll_to_message(
    
    317
    +                meta_row.log_line_id,
    
    318
    +                meta_row.time)
    
    319
    +
    
    320
    +    @staticmethod
    
    321
    +    def _get_date_from_timestamp(timestamp: float) -> datetime:
    
    322
    +        localtime = time.localtime(timestamp)
    
    323
    +        year, month, day = localtime[0], localtime[1], localtime[2]
    
    324
    +        date = datetime(year, month, day)
    
    325
    +        return date
    
    326
    +
    
    205 327
         @staticmethod
    
    206 328
         def _get_accounts() -> dict[int, str]:
    
    207 329
             accounts: dict[int, str] = {}
    
    ... ... @@ -232,7 +354,14 @@ def set_focus(self) -> None:
    232 354
         def set_context(self, account: Optional[str], jid: Optional[JID]) -> None:
    
    233 355
             self._account = account
    
    234 356
             self._jid = jid
    
    235
    -        self._ui.search_checkbutton.set_active(jid is None)
    
    357
    +        everywhere = bool(jid is None)
    
    358
    +        self._ui.search_checkbutton.set_active(everywhere)
    
    359
    +        context = account is not None and jid is not None
    
    360
    +        if not context or everywhere:
    
    361
    +            self._scope = 'everywhere'
    
    362
    +        else:
    
    363
    +            self._scope = 'contact'
    
    364
    +        self._update_calendar()
    
    236 365
     
    
    237 366
     
    
    238 367
     class RowHeader(Gtk.Box):