[gnome-contacts/nielsdg/gtk4: 1/2] Port to GTK4 and libadwaita




commit 0733806c026504db7f644abd2d04385f54161773
Author: Niels De Graef <nielsdegraef gmail com>
Date:   Thu Oct 14 01:19:13 2021 +0200

    Port to GTK4 and libadwaita
    
    This is a mega-commit which ports Contacts to GTK4 and libadwaita, the
    library which provides GNOME-specific widgets on top of GTK4.
    
    This change also now follows the new mockups of Contacts, which use a
    boxed list style to convey contact information.
    
    There is a minor set of known issues which we'll still need to solve
    later (preferably):
    
    * For now, taking a picture with your webcam is not implemented. In
      GTK3, we used to do this with Cheese, but this hasn't been ported to
      GTK4. Ideally, we could just directly use Pipewire though.
    * Some CRITICALs when we have some unexpectedly long names or property
      values
    * The delete button is gone for most properties. This probably needs to
      be rethough at the design level on how we want to deal with it.
    * We're still blocked a bit on libedataserverui not having a GTK4 port
      yet.

 data/contacts.gresource.xml                |    9 +-
 data/gtk/help-overlay.ui                   |    8 -
 data/icons/birthday-symbolic.svg           |    8 +
 data/icons/external-link-symbolic.svg      |   11 +
 data/icons/note-symbolic.svg               |    4 +
 data/icons/photo-camera-symbolic.svg       |    4 +
 data/ui/contacts-accounts-list.ui          |   15 +-
 data/ui/contacts-avatar-selector.ui        |  130 +--
 data/ui/contacts-contact-pane.ui           |   96 +--
 data/ui/contacts-crop-cheese-dialog.ui     |  116 ---
 data/ui/contacts-crop-dialog.ui            |   34 +
 data/ui/contacts-editor-menu.ui            |    2 -
 data/ui/contacts-in-app-notification.ui    |   54 --
 data/ui/contacts-link-suggestion-grid.ui   |   42 +-
 data/ui/contacts-linked-personas-dialog.ui |   17 +-
 data/ui/contacts-list-pane.ui              |   62 +-
 data/ui/contacts-main-window.ui            |  641 +++++----------
 data/ui/contacts-setup-window.ui           |   92 +--
 data/ui/style.css                          |  112 ++-
 docs/meson.build                           |    4 +-
 meson.build                                |   14 +-
 meson_options.txt                          |    2 +-
 po/POTFILES.in                             |    4 +-
 po/POTFILES.skip                           |    2 +-
 src/cc-crop-area.c                         | 1234 +++++++++++++---------------
 src/cc-crop-area.h                         |   19 +-
 src/contacts-accounts-list.vala            |  100 +--
 src/contacts-addressbook-dialog.vala       |   26 +-
 src/contacts-addressbook-list.vala         |   45 +-
 src/contacts-app.vala                      |   79 +-
 src/contacts-avatar-selector.vala          |  239 ++----
 src/contacts-avatar.vala                   |   90 +-
 src/contacts-contact-editor.vala           |   32 +-
 src/contacts-contact-list.vala             |  287 ++++---
 src/contacts-contact-pane.vala             |  106 ++-
 src/contacts-contact-sheet.vala            |  393 +++++----
 src/contacts-crop-cheese-dialog.vala       |  111 ---
 src/contacts-crop-dialog.vala              |   43 +
 src/contacts-delete-operation.vala         |   55 ++
 src/contacts-editor-persona.vala           |  113 +--
 src/contacts-editor-property.vala          |  610 +++++++-------
 src/contacts-esd-setup.vala                |   33 +-
 src/contacts-fake-persona-store.vala       |    2 +-
 src/contacts-in-app-notification.vala      |   77 --
 src/contacts-link-operation.vala           |   79 ++
 src/contacts-link-suggestion-grid.vala     |   16 +-
 src/contacts-linked-personas-dialog.vala   |   18 +-
 src/contacts-linking.vala                  |  103 ---
 src/contacts-list-pane.vala                |   37 +-
 src/contacts-main-window.vala              |  485 ++++-------
 src/contacts-operation.vala                |   63 ++
 src/contacts-setup-window.vala             |   12 +-
 src/contacts-type-combo.vala               |   70 +-
 src/contacts-typeset.vala                  |  156 ++--
 src/contacts-unlink-operation.vala         |   56 ++
 src/contacts-utils.vala                    |   84 +-
 src/main.vala                              |    2 +
 src/meson.build                            |   29 +-
 vapi/custom.vapi                           |   16 +-
 59 files changed, 2873 insertions(+), 3530 deletions(-)
---
diff --git a/data/contacts.gresource.xml b/data/contacts.gresource.xml
index 370e2ccb..20343b5f 100644
--- a/data/contacts.gresource.xml
+++ b/data/contacts.gresource.xml
@@ -2,13 +2,18 @@
 <gresources>
   <gresource prefix="/org/gnome/Contacts">
     <file compressed="true">ui/style.css</file>
+
+    <file preprocess="xml-stripblanks">icons/birthday-symbolic.svg</file>
+    <file preprocess="xml-stripblanks">icons/external-link-symbolic.svg</file>
+    <file preprocess="xml-stripblanks">icons/note-symbolic.svg</file>
+    <file preprocess="xml-stripblanks">icons/photo-camera-symbolic.svg</file>
+
     <file compressed="true" preprocess="xml-stripblanks">gtk/help-overlay.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/contacts-accounts-list.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/contacts-avatar-selector.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/contacts-contact-pane.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks">ui/contacts-crop-cheese-dialog.ui</file>
+    <file compressed="true" preprocess="xml-stripblanks">ui/contacts-crop-dialog.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/contacts-editor-menu.ui</file>
-    <file compressed="true" preprocess="xml-stripblanks">ui/contacts-in-app-notification.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/contacts-link-suggestion-grid.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/contacts-linked-personas-dialog.ui</file>
     <file compressed="true" preprocess="xml-stripblanks">ui/contacts-list-pane.ui</file>
diff --git a/data/gtk/help-overlay.ui b/data/gtk/help-overlay.ui
index 2b8a7a81..91a4057f 100644
--- a/data/gtk/help-overlay.ui
+++ b/data/gtk/help-overlay.ui
@@ -2,49 +2,41 @@
   <object class="GtkShortcutsWindow" id="help_overlay">
     <child>
       <object class="GtkShortcutsSection">
-        <property name="visible">1</property>
         <child>
           <object class="GtkShortcutsGroup">
-            <property name="visible">1</property>
             <property name="title" translatable="yes" context="shortcut window">Overview</property>
             <child>
               <object class="GtkShortcutsShortcut">
-                <property name="visible">1</property>
                 <property name="accelerator">F1</property>
                 <property name="title" translatable="yes" context="shortcut window">Help</property>
               </object>
             </child>
             <child>
               <object class="GtkShortcutsShortcut">
-                <property name="visible">1</property>
                 <property name="accelerator">F10</property>
                 <property name="title" translatable="yes" context="shortcut window">Open menu</property>
               </object>
             </child>
             <child>
               <object class="GtkShortcutsShortcut">
-                <property name="visible">1</property>
                 <property name="accelerator">&lt;Primary&gt;n</property>
                 <property name="title" translatable="yes" context="shortcut window">Create a new 
contact</property>
               </object>
             </child>
             <child>
               <object class="GtkShortcutsShortcut">
-                <property name="visible">1</property>
                 <property name="accelerator">&lt;Primary&gt;f &lt;Primary&gt;s</property>
                 <property name="title" translatable="yes" context="shortcut window">Search</property>
               </object>
             </child>
             <child>
               <object class="GtkShortcutsShortcut">
-                <property name="visible">1</property>
                 <property name="accelerator">&lt;Primary&gt;question</property>
                 <property name="title" translatable="yes" context="shortcut window">Shortcut list</property>
               </object>
             </child>
             <child>
               <object class="GtkShortcutsShortcut">
-                <property name="visible">1</property>
                 <property name="accelerator">&lt;Primary&gt;q</property>
                 <property name="title" translatable="yes" context="shortcut window">Quit</property>
               </object>
diff --git a/data/icons/birthday-symbolic.svg b/data/icons/birthday-symbolic.svg
new file mode 100644
index 00000000..2924f38b
--- /dev/null
+++ b/data/icons/birthday-symbolic.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg";>
+    <g fill="#2e3436">
+        <path d="m 1 9 v 2 l 3.5 1 l 3.5 -1 l 3.5 1 l 3.5 -1 v -2 c 0 -2 -2 -2 -2 -2 h -10 s -2 0 -2 2 z m 7 
3 l -3.5 1 l -3.5 -1 v 4 h 14 v -4 l -3.5 1 z m 0 0"/>
+        <path d="m 7 4 h 2 v 4 h -2 z m 0 0"/>
+        <path d="m 8 0.507812 s -1.058594 0.898438 -0.996094 1.496094 c 0.042969 0.464844 0.527344 0.996094 
0.996094 0.996094 s 0.953125 -0.53125 0.996094 -0.996094 c 0.0625 -0.597656 -0.996094 -1.496094 -0.996094 
-1.496094 z m 0 0"/>
+    </g>
+</svg>
diff --git a/data/icons/external-link-symbolic.svg b/data/icons/external-link-symbolic.svg
new file mode 100644
index 00000000..1ac7ee70
--- /dev/null
+++ b/data/icons/external-link-symbolic.svg
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg";>
+    <g fill="#2e3436">
+        <path d="m 2 3 v 11 h 11 v -4 h -2 v 2 h -7 v -7 h 2 v -2 z m 0 0"/>
+        <path d="m 9 2 c -0.550781 0 -1 0.449219 -1 1 s 0.449219 1 1 1 h 3 v 3 c 0 0.550781 0.449219 1 1 1 s 
1 -0.449219 1 -1 v -4 c 0 -0.550781 -0.449219 -1 -1 -1 z m 0 0"/>
+        <path d="m 13 2 h 1 v 1 h -1 z m 0 0"/>
+        <path d="m 12.292969 2.289062 l -4.5 4.46875 c -0.390625 0.390626 -0.390625 1.027344 0 1.414063 c 
0.390625 0.394531 1.023437 0.394531 1.414062 0.007813 l 4.5 -4.46875 c 0.390625 -0.390626 0.390625 -1.027344 
0 -1.414063 c -0.386719 -0.394531 -1.019531 -0.394531 -1.414062 -0.007813 z m 0 0"/>
+        <path d="m 13 7 h 1 v 1 h -1 z m 0 0"/>
+        <path d="m 8 2 h 1 v 1 h -1 z m 0 0"/>
+    </g>
+</svg>
diff --git a/data/icons/note-symbolic.svg b/data/icons/note-symbolic.svg
new file mode 100644
index 00000000..ca8c9497
--- /dev/null
+++ b/data/icons/note-symbolic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg";>
+    <path d="m 3.121094 2 c -1.175782 0 -2.121094 0.945312 -2.121094 2.121094 v 6.757812 c 0 1.175782 
0.945312 2.121094 2.121094 2.121094 h 5.878906 l 4 3 v -3.011719 c 1.117188 -0.0625 2 -0.976562 2 -2.109375 v 
-6.757812 c 0 -1.175782 -0.945312 -2.121094 -2.121094 -2.121094 z m -0.121094 3 h 10 v 1 h -10 z m 0 2 h 10 v 
1 h -10 z m 0 2 h 10 v 1 h -10 z m 0 0" fill="#2e3436"/>
+</svg>
diff --git a/data/icons/photo-camera-symbolic.svg b/data/icons/photo-camera-symbolic.svg
new file mode 100644
index 00000000..4d99fa5b
--- /dev/null
+++ b/data/icons/photo-camera-symbolic.svg
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg";>
+    <path d="m 6 2 c -0.550781 0 -1 0.449219 -1 1 v 1 h -3 c -0.550781 0 -1 0.449219 -1 1 v 8 c 0 0.550781 
0.449219 1 1 1 h 12 c 0.550781 0 1 -0.449219 1 -1 v -8 c 0 -0.550781 -0.449219 -1 -1 -1 h -3 v -1 c 0 
-0.550781 -0.449219 -1 -1 -1 z m 2 3 c 2.210938 0 4 1.789062 4 4 s -1.789062 4 -4 4 c -2.207031 0 -4 
-1.789062 -4 -4 s 1.792969 -4 4 -4 z m 0 2 c -1.105469 0 -2 0.894531 -2 2 s 0.894531 2 2 2 s 2 -0.894531 2 -2 
s -0.894531 -2 -2 -2 z m 0 0" fill="#2e3436"/>
+</svg>
diff --git a/data/ui/contacts-accounts-list.ui b/data/ui/contacts-accounts-list.ui
index d08d297e..b95ce630 100644
--- a/data/ui/contacts-accounts-list.ui
+++ b/data/ui/contacts-accounts-list.ui
@@ -1,11 +1,16 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <!-- interface-requires gtk+ 3.22 -->
-  <template class="ContactsAccountsList" parent="GtkListBox">
-    <property name="visible">True</property>
-    <property name="selection_mode">none</property>
+  <template class="ContactsAccountsList" parent="AdwBin">
     <style>
-      <class name="content"/>
+      <class name="contacts-accounts-list"/>
     </style>
+    <child>
+      <object class="GtkListBox" id="listbox">
+        <property name="selection_mode">none</property>
+        <style>
+          <class name="boxed-list"/>
+        </style>
+      </object>
+    </child>
   </template>
 </interface>
diff --git a/data/ui/contacts-avatar-selector.ui b/data/ui/contacts-avatar-selector.ui
index 336b6baf..4872e0d4 100644
--- a/data/ui/contacts-avatar-selector.ui
+++ b/data/ui/contacts-avatar-selector.ui
@@ -1,124 +1,84 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <requires lib="gtk+" version="3.22"/>
-  <template class="ContactsAvatarSelector" parent="GtkWindow">
-    <property name="can_focus">False</property>
+  <template class="ContactsAvatarSelector" parent="GtkDialog">
     <property name="modal">True</property>
     <property name="default_width">400</property>
     <property name="default_height">400</property>
-    <property name="destroy_with_parent">True</property>
-    <property name="skip_taskbar_hint">True</property>
-    <signal name="delete-event" handler="on_delete_event" swapped="no"/>
-    <child type="titlebar">
-      <object class="GtkHeaderBar">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
-        <property name="can_focus">False</property>
-        <child>
-          <object class="GtkButton">
-            <property name="label" translatable="yes">Cancel</property>
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <property name="receives_default">True</property>
-            <signal name="clicked" handler="on_cancel_clicked" swapped="no"/>
-          </object>
-        </child>
-        <child>
-          <object class="GtkButton">
-            <property name="label" translatable="yes">Done</property>
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <property name="receives_default">True</property>
-            <signal name="clicked" handler="on_done_clicked" swapped="no"/>
-            <style>
-              <class name="suggested-action"/>
-            </style>
-          </object>
-          <packing>
-            <property name="pack_type">end</property>
-          </packing>
-        </child>
+    <property name="title" translatable="yes">Select a new avatar</property>
+
+    <child type="action">
+      <object class="GtkButton" id="cancel_button">
+        <property name="label" translatable="yes">Cancel</property>
+        <property name="receives_default">True</property>
+      </object>
+    </child>
+    <child type="action">
+      <object class="GtkButton" id="done_button">
+        <property name="label" translatable="yes">Done</property>
+        <style>
+          <class name="suggested-action"/>
+        </style>
       </object>
     </child>
-    <child>
+    <action-widgets>
+      <action-widget response="cancel">cancel_button</action-widget>
+      <action-widget response="accept" default="true">done_button</action-widget>
+    </action-widgets>
+
+
+    <child internal-child="content_area">
       <object class="GtkBox">
-        <property name="visible">True</property>
-        <property name="can_focus">False</property>
         <property name="orientation">vertical</property>
         <child>
           <object class="GtkScrolledWindow">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
             <property name="hscrollbar_policy">never</property>
             <property name="hexpand">True</property>
             <property name="vexpand">True</property>
             <child>
-              <object class="GtkBox">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="margin_left">10</property>
-                <property name="margin_right">10</property>
-                <property name="margin_top">10</property>
-                <property name="margin_bottom">10</property>
-                <property name="orientation">vertical</property>
-                <property name="spacing">10</property>
+              <object class="GtkViewport">
+                <property name="scroll-to-focus">True</property>
                 <child>
-                  <object class="GtkFlowBox" id="thumbnail_grid">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="column_spacing">5</property>
-                    <property name="row_spacing">5</property>
-                    <property name="max_children_per_line">8</property>
-                    <property name="selection_mode">single</property>
-                    <property name="homogeneous">False</property>
+                  <object class="GtkBox">
+                    <property name="margin-start">10</property>
+                    <property name="margin-end">10</property>
+                    <property name="margin-top">10</property>
+                    <property name="margin-bottom">10</property>
+                    <property name="orientation">vertical</property>
+                    <property name="spacing">10</property>
+                    <child>
+                      <object class="GtkFlowBox" id="thumbnail_grid">
+                        <property name="column_spacing">5</property>
+                        <property name="row_spacing">5</property>
+                        <property name="max_children_per_line">8</property>
+                        <property name="selection_mode">single</property>
+                        <property name="homogeneous">False</property>
+                      </object>
+                    </child>
                   </object>
-                  <packing>
-                    <property name="expand">True</property>
-                    <property name="fill">True</property>
-                    <property name="position">1</property>
-                  </packing>
                 </child>
               </object>
             </child>
           </object>
         </child>
         <child>
-          <object class="GtkSeparator">
-            <property name="visible">True</property>
-          </object>>
-        </child>
-        <child>
-          <object class="GtkBox">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="margin_left">10</property>
-            <property name="margin_right">10</property>
-            <property name="margin_top">10</property>
-            <property name="margin_bottom">10</property>
-            <property name="spacing">10</property>
+          <object class="GtkActionBar">
             <property name="halign">center</property>
             <child>
-              <object class="GtkButton" id="cheese_button">
+              <object class="GtkButton" id="camera_button">
+                <property name="visible">False</property>
                 <property name="label" translatable="yes">Take a Picture…</property>
-                <property name="can_focus">True</property>
-                <property name="no_show_all">True</property>
                 <property name="receives_default">True</property>
-                <signal name="clicked" handler="on_cheese_clicked" swapped="no"/>
+                <signal name="clicked" handler="on_camera_button_clicked"/>
               </object>
             </child>
             <child>
               <object class="GtkButton">
                 <property name="label" translatable="yes">Select a File…</property>
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
                 <property name="receives_default">True</property>
-                <signal name="clicked" handler="on_file_clicked" swapped="no"/>
+                <signal name="clicked" handler="on_file_clicked"/>
               </object>
             </child>
           </object>
-          <packing>
-            <property name="pack_type">end</property>
-          </packing>
         </child>
       </object>
     </child>
diff --git a/data/ui/contacts-contact-pane.ui b/data/ui/contacts-contact-pane.ui
index b75c3be1..818d4f50 100644
--- a/data/ui/contacts-contact-pane.ui
+++ b/data/ui/contacts-contact-pane.ui
@@ -1,80 +1,68 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <requires lib="gtk+" version="3.20"/>
-  <template class="ContactsContactPane" parent="GtkBin">
-    <property name="visible">True</property>
+  <template class="ContactsContactPane" parent="AdwBin">
     <property name="hexpand">True</property>
     <property name="vexpand">True</property>
     <child>
       <object class="GtkStack" id="stack">
-        <property name="visible">True</property>
-        <property name="visible-child">none_selected_page</property>
         <child>
-          <object class="HdyStatusPage" id="none_selected_page">
-            <property name="visible">True</property>
-            <property name="hexpand">True</property>
-            <property name="vexpand">True</property>
-            <property name="icon_name">avatar-default-symbolic</property>
-            <property name="title" translatable="yes">Select a Contact</property>
-          </object>
-          <packing>
+          <object class="GtkStackPage" id="none_selected_page">
             <property name="name">none-selected-page</property>
-          </packing>
+            <property name="child">
+              <object class="AdwStatusPage">
+                <property name="icon_name">avatar-default-symbolic</property>
+                <property name="title" translatable="yes">Select a Contact</property>
+              </object>
+            </property>
+          </object>
         </child>
         <child>
-          <object class="GtkScrolledWindow" id="contact_sheet_view">
-            <property name="visible">True</property>
-            <property name="hexpand">True</property>
-            <property name="vexpand">True</property>
-            <property name="shadow_type">none</property>
-            <property name="hscrollbar_policy">never</property>
-            <property name="vscrollbar_policy">automatic</property>
-            <child>
-              <object class="HdyClamp">
-                <property name="visible">True</property>
-                <property name="margin-top">32</property>
-                <property name="margin-bottom">32</property>
-                <property name="margin-left">24</property>
-                <property name="margin-right">24</property>
+          <object class="GtkStackPage" id="contact_sheet_page">
+            <property name="name">contact-sheet-page</property>
+            <property name="child">
+              <object class="GtkScrolledWindow" id="contact_sheet_view">
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <property name="hscrollbar_policy">never</property>
+                <property name="vscrollbar_policy">automatic</property>
                 <child>
-                  <object class="GtkBox" id="contact_sheet_page">
-                    <property name="visible">True</property>
+                  <object class="AdwClamp" id="contact_sheet_clamp">
+                    <property name="maximum-size">500</property>
+                    <style>
+                      <class name="contacts-sheet-container"/>
+                    </style>
                   </object>
                 </child>
               </object>
-            </child>
+            </property>
           </object>
-          <packing>
-            <property name="name">contact-sheet-page</property>
-          </packing>
         </child>
         <child>
-          <object class="GtkScrolledWindow" id="contact_editor_view">
-            <property name="visible">True</property>
-            <property name="hexpand">True</property>
-            <property name="vexpand">True</property>
-            <property name="shadow_type">none</property>
-            <property name="hscrollbar_policy">never</property>
-            <property name="vscrollbar_policy">automatic</property>
-            <child>
-              <object class="HdyClamp">
-                <property name="visible">True</property>
-                <property name="margin-top">32</property>
-                <property name="margin-bottom">32</property>
-                <property name="margin-left">24</property>
-                <property name="margin-right">24</property>
+          <object class="GtkStackPage" id="contact_editor_page">
+            <property name="name">contact-editor-page</property>
+            <property name="child">
+              <object class="GtkScrolledWindow" id="contact_editor_view">
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <property name="hscrollbar_policy">never</property>
+                <property name="vscrollbar_policy">automatic</property>
                 <child>
-                  <object class="GtkBox" id="contact_editor_page">
-                    <property name="visible">True</property>
+                  <object class="AdwClamp" id="contact_editor_clamp">
+                    <style>
+                      <class name="contacts-contact-editor-container"/>
+                    </style>
+                    <property name="maximum-size" bind-source="contact_sheet_clamp" 
bind-property="maximum-size" bind-flags="sync-create"/>
+                    <child>
+                      <object class="GtkBox" id="contact_editor_box">
+                      </object>
+                    </child>
                   </object>
                 </child>
               </object>
-            </child>
+            </property>
           </object>
-          <packing>
-            <property name="name">contact-editor-page</property>
-          </packing>
         </child>
+        <property name="visible-child-name">none-selected-page</property>
       </object>
     </child>
   </template>
diff --git a/data/ui/contacts-crop-dialog.ui b/data/ui/contacts-crop-dialog.ui
new file mode 100644
index 00000000..2a795367
--- /dev/null
+++ b/data/ui/contacts-crop-dialog.ui
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <template class="ContactsCropDialog" parent="GtkDialog">
+    <property name="modal">True</property>
+    <property name="default-width">400</property>
+    <property name="default-height">400</property>
+    <property name="destroy-with-parent">True</property>
+    <property name="title"></property>
+
+    <child type="action">
+      <object class="GtkButton" id="cancel_button">
+        <property name="label" translatable="yes">Cancel</property>
+      </object>
+    </child>
+    <child type="action">
+      <object class="GtkButton" id="done_button">
+        <property name="label" translatable="yes">Done</property>
+        <style>
+          <class name="suggested-action"/>
+        </style>
+      </object>
+    </child>
+    <action-widgets>
+      <action-widget response="cancel">cancel_button</action-widget>
+      <action-widget response="accept" default="true">done_button</action-widget>
+    </action-widgets>
+
+    <child internal-child="content_area">
+      <object class="GtkBox" id="box">
+        <property name="orientation">vertical</property>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/data/ui/contacts-editor-menu.ui b/data/ui/contacts-editor-menu.ui
index 21ac6bb2..92eb32a3 100644
--- a/data/ui/contacts-editor-menu.ui
+++ b/data/ui/contacts-editor-menu.ui
@@ -3,12 +3,10 @@
   <object class="GtkPopoverMenu" id="editor_menu">
     <child>
       <object class="GtkBox">
-        <property name="visible">True</property>
         <property name="orientation">vertical</property>
         <property name="margin">10</property>
         <child>
           <object class="GtkModelButton">
-            <property name="visible">True</property>
             <property name="action-name">persona.change-addressbook</property>
             <property name="text" translatable="yes">Change Addressbook</property>
           </object>
diff --git a/data/ui/contacts-link-suggestion-grid.ui b/data/ui/contacts-link-suggestion-grid.ui
index 30511c78..8b5e34e8 100644
--- a/data/ui/contacts-link-suggestion-grid.ui
+++ b/data/ui/contacts-link-suggestion-grid.ui
@@ -1,26 +1,24 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <!-- interface-requires gtk+ 3.22 -->
   <template class="ContactsLinkSuggestionGrid" parent="GtkGrid">
     <property name="orientation">horizontal</property>
-    <property name="visible">True</property>
-    <property name="valign">end</property>
     <property name="column_spacing">6</property>
+    <style>
+      <class name="contacts-link-suggestion"/>
+    </style>
     <child>
       <object class="GtkLabel" id="description_label">
-        <property name="visible">True</property>
         <property name="valign">end</property>
         <property name="halign">start</property>
         <property name="hexpand">True</property>
-        <property name="margin_top">12</property>
+        <property name="margin-top">12</property>
         <property name="wrap">True</property>
         <property name="wrap_mode">word-char</property>
+        <layout>
+          <property name="row">0</property>
+          <property name="column">1</property>
+        </layout>
       </object>
-      <packing>
-        <property name="left_attach">1</property>
-        <property name="top_attach">0</property>
-        <property name="height">1</property>
-      </packing>
     </child>
     <child>
       <object class="GtkLabel" id="extra_info_label">
@@ -32,16 +30,14 @@
         <style>
           <class name="dim-label"/>
         </style>
+        <layout>
+          <property name="row">1</property>
+          <property name="column">1</property>
+        </layout>
       </object>
-      <packing>
-        <property name="left_attach">1</property>
-        <property name="top_attach">1</property>
-        <property name="height">1</property>
-      </packing>
     </child>
     <child>
       <object class="GtkBox">
-        <property name="visible">True</property>
         <property name="valign">center</property>
         <property name="orientation">horizontal</property>
         <property name="spacing">6</property>
@@ -51,7 +47,6 @@
         <property name="margin_end">6</property>
         <child>
           <object class="GtkButton" id="accept_button">
-            <property name="visible">True</property>
             <property name="valign">center</property>
             <property name="label" translatable="yes">Link Contacts</property>
             <property name="margin_end">6</property>
@@ -59,31 +54,28 @@
         </child>
         <child>
           <object class="GtkSeparator">
-            <property name="visible">True</property>
             <property name="orientation">vertical</property>
           </object>
         </child>
         <child>
           <object class="GtkButton" id="reject_button">
-            <property name="visible">True</property>
             <property name="valign">center</property>
             <style>
               <class name="flat"/>
             </style>
             <child>
               <object class="GtkImage">
-                <property name="visible">True</property>
                 <property name="icon_name">window-close-symbolic</property>
               </object>
             </child>
           </object>
         </child>
+        <layout>
+          <property name="row">0</property>
+          <property name="column">2</property>
+          <property name="row-span">2</property>
+        </layout>
       </object>
-      <packing>
-        <property name="left_attach">2</property>
-        <property name="top_attach">0</property>
-        <property name="height">2</property>
-      </packing>
     </child>
   </template>
 </interface>
diff --git a/data/ui/contacts-linked-personas-dialog.ui b/data/ui/contacts-linked-personas-dialog.ui
index 16e46105..e783d52e 100644
--- a/data/ui/contacts-linked-personas-dialog.ui
+++ b/data/ui/contacts-linked-personas-dialog.ui
@@ -1,46 +1,43 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <requires lib="gtk+" version="3.22"/>
   <template class="ContactsLinkedPersonasDialog" parent="GtkDialog">
-    <property name="visible">True</property>
     <property name="modal">True</property>
     <property name="default_width">600</property>
     <property name="default_height">400</property>
     <child type="titlebar">
       <object class="GtkHeaderBar">
-        <property name="visible">True</property>
         <property name="subtitle" translatable="yes">Linked Accounts</property>
       </object>
     </child>
     <child internal-child="vbox">
       <object class="GtkBox">
-        <property name="visible">True</property>
         <property name="orientation">vertical</property>
         <child>
           <object class="GtkGrid">
-            <property name="visible">True</property>
             <property name="orientation">vertical</property>
             <property name="column_spacing">8</property>
             <property name="row_spacing">12</property>
             <child>
               <object class="GtkScrolledWindow">
-                <property name="visible">True</property>
                 <property name="hscrollbar_policy">never</property>
                 <property name="vscrollbar_policy">automatic</property>
                 <property name="hexpand">True</property>
                 <property name="vexpand">True</property>
                 <property name="shadow_type">in</property>
                 <child>
-                  <object class="GtkListBox" id="linked_accounts_view">
-                    <property name="visible">True</property>
-                    <property name="selection_mode">none</property>
+                  <object class="GtkViewport">
+                    <property name="scroll-to-focus">True</property>
+                    <child>
+                      <object class="GtkListBox" id="linked_accounts_view">
+                        <property name="selection_mode">none</property>
+                      </object>
+                    </child>
                   </object>
                 </child>
               </object>
             </child>
             <child>
               <object class="GtkLabel">
-                <property name="visible">True</property>
                 <property name="halign">center</property>
                 <property name="label" translatable="yes">You can link contacts by selecting them from the 
contacts list</property>
               </object>
diff --git a/data/ui/contacts-list-pane.ui b/data/ui/contacts-list-pane.ui
index 00ec107b..e67fa216 100644
--- a/data/ui/contacts-list-pane.ui
+++ b/data/ui/contacts-list-pane.ui
@@ -1,89 +1,47 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.15.2 on Thu Aug 15 15:33:02 2013 -->
 <interface>
-  <!-- interface-requires gtk+ 3.10 -->
-  <template class="ContactsListPane" parent="GtkFrame">
-    <property name="can_focus">False</property>
-    <property name="hexpand">False</property>
-    <property name="hexpand_set">True</property>
-    <property name="shadow_type">none</property>
+  <template class="ContactsListPane" parent="AdwBin">
     <child>
-      <object class="GtkGrid">
+      <object class="GtkBox" id="box">
         <property name="orientation">vertical</property>
-        <property name="visible">True</property>
         <child>
           <object class="GtkSearchEntry" id="filter_entry">
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
-            <property name="primary_icon_name">edit-find-symbolic</property>
-            <property name="primary_icon_activatable">False</property>
-            <property name="primary_icon_sensitive">False</property>
-            <property name="margin">6</property>
-            <property name="placeholder_text" translatable="yes">Type to search</property>
-            <signal name="search_changed" handler="filter_entry_changed" object="ContactsListPane" 
after="no" swapped="no"/>
+            <property name="placeholder-text" translatable="yes">Type to search</property>
+            <signal name="search-changed" handler="filter_entry_changed"/>
+            <style>
+              <class name="contacts-filter-entry"/>
+            </style>
           </object>
-          <packing>
-            <property name="left_attach">0</property>
-            <property name="top_attach">0</property>
-          </packing>
         </child>
         <child>
-          <object class="GtkScrolledWindow" id="contacts_list_container">
-            <property name="visible">True</property>
-            <property name="can_focus">True</property>
+          <object class="AdwBin" id="contacts_list_container">
             <property name="hexpand">True</property>
             <property name="vexpand">True</property>
-            <property name="hscrollbar_policy">never</property>
-            <property name="no_show_all">True</property>
           </object>
-          <packing>
-            <property name="left_attach">0</property>
-            <property name="top_attach">1</property>
-            <property name="width">1</property>
-            <property name="height">1</property>
-          </packing>
         </child>
         <child>
           <object class="GtkActionBar" id="actions_bar">
-            <property name="visible">False</property>
+            <property name="revealed">False</property>
             <child>
               <object class="GtkButton" id="link_button">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
                 <property name="focus_on_click">False</property>
                 <property name="label" translatable="yes" comments="Link refers to the verb, from linking 
contacts together">Link</property>
-                <property name="width_request">70</property>
                 <property name="sensitive">False</property>
                 <signal name="clicked" handler="on_link_button_clicked"/>
               </object>
-              <packing>
-                <property name="pack_type">start</property>
-              </packing>
             </child>
-            <child>
+            <child type="end">
               <object class="GtkButton" id="delete_button">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
                 <property name="focus_on_click">False</property>
                 <property name="label" translatable="yes">Remove</property>
-                <property name="width_request">70</property>
                 <property name="sensitive">False</property>
                 <signal name="clicked" handler="on_delete_button_clicked"/>
                 <style>
                   <class name="destructive-action"/>
                 </style>
               </object>
-              <packing>
-                <property name="pack_type">end</property>
-              </packing>
             </child>
           </object>
-          <packing>
-            <property name="left_attach">0</property>
-            <property name="top_attach">2</property>
-            <property name="width">1</property>
-            <property name="height">1</property>
-          </packing>
         </child>
       </object>
     </child>
diff --git a/data/ui/contacts-main-window.ui b/data/ui/contacts-main-window.ui
index ee0cfd1f..41b99e89 100644
--- a/data/ui/contacts-main-window.ui
+++ b/data/ui/contacts-main-window.ui
@@ -1,459 +1,240 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <!-- interface-requires gtk+ 3.22 -->
-  <object class="GtkPopoverMenu" id="hamburger_menu_popover">
-    <child>
-      <object class="GtkBox" id="hamburger_menu_box">
-        <property name="visible">True</property>
-        <property name="margin">12</property>
-        <property name="orientation">vertical</property>
-        <child>
-          <object class="GtkLabel">
-            <property name="visible">True</property>
-            <property name="margin">5</property>
-            <property name="halign">start</property>
-            <property name="label" translatable="yes">List Contacts By:</property>
-            <attributes>
-              <attribute name="weight" value="bold"/>
-              <attribute name="scale" value="0.8"/>
-            </attributes>
-            <style>
-              <class name="dim-label"/>
-            </style>
-          </object>
-        </child>
-        <child>
-          <object class="GtkModelButton" id="sort_on_firstname_button">
-            <property name="visible">True</property>
-            <property name="text" translatable="yes">First Name</property>
-            <property name="role">radio</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkModelButton" id="sort_on_surname_button">
-            <property name="visible">True</property>
-            <property name="text" translatable="yes">Surname</property>
-            <property name="role">radio</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkSeparator">
-            <property name="margin-top">6</property>
-            <property name="margin-bottom">6</property>
-            <property name="visible">True</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkModelButton">
-            <property name="can_focus">True</property>
-            <property name="text" translatable="yes">Change Address Book…</property>
-            <property name="action-name">app.change-book</property>
-            <property name="visible">True</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkModelButton">
-            <property name="can_focus">True</property>
-            <property name="text" translatable="yes">Online Accounts &lt;sup&gt;↗&lt;/sup&gt;</property>
-            <property name="action-name">app.online-accounts</property>
-            <property name="use-markup">True</property>
-            <property name="visible">True</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkSeparator">
-            <property name="margin-top">6</property>
-            <property name="margin-bottom">6</property>
-            <property name="visible">True</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkModelButton">
-            <property name="can_focus">True</property>
-            <property name="text" translatable="yes">Keyboard Shortcuts</property>
-            <property name="action-name">win.show-help-overlay</property>
-            <property name="visible">True</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkModelButton">
-            <property name="can_focus">True</property>
-            <property name="text" translatable="yes">Help</property>
-            <property name="action-name">app.help</property>
-            <property name="visible">True</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkModelButton">
-            <property name="can_focus">True</property>
-            <property name="text" translatable="yes">About Contacts</property>
-            <property name="action-name">app.about</property>
-            <property name="visible">True</property>
-          </object>
-        </child>
-      </object>
-    </child>
-  </object>
-  <object class="GtkPopoverMenu" id="contact_sheet_menu">
+  <menu id="hamburger_menu_popover">
+    <section>
+      <attribute name="label" translatable="yes">List Contacts By:</attribute>
+      <item>
+        <attribute name="label" translatable="yes">First Name</attribute>
+        <attribute name="action">window.sort-on</attribute>
+        <attribute name="target">firstname</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Surname</attribute>
+        <attribute name="action">window.sort-on</attribute>
+        <attribute name="target">surname</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">Change Address Book…</attribute>
+        <attribute name="action">app.change-book</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Online Accounts &lt;sup&gt;↗&lt;/sup&gt;</attribute>
+        <attribute name="action">app.online-accounts</attribute>
+        <attribute name="use-markup">True</attribute>
+      </item>
+    </section>
+    <section>
+      <item>
+        <attribute name="label" translatable="yes">Keyboard Shortcuts</attribute>
+        <attribute name="action">win.show-help-overlay</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">Help</attribute>
+        <attribute name="action">app.help</attribute>
+      </item>
+      <item>
+        <attribute name="label" translatable="yes">About Contacts</attribute>
+        <attribute name="action">app.about</attribute>
+      </item>
+    </section>
+  </menu>
+
+  <template class="ContactsMainWindow" parent="AdwApplicationWindow">
+    <property name="default_width">800</property>
+    <property name="default_height">600</property>
+    <property name="icon_name">gnome-contacts</property>
+
     <child>
-      <object class="GtkBox">
-        <property name="visible">True</property>
-        <property name="orientation">vertical</property>
-        <property name="margin">10</property>
-        <child>
-          <object class="GtkModelButton">
-            <property name="visible">False</property>
-            <property name="action-name">window.share-contact</property>
-            <property name="text" translatable="yes">Share</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkModelButton">
-            <property name="visible">True</property>
-            <property name="action-name">window.edit-contact</property>
-            <property name="text" translatable="yes">Edit</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkModelButton" id="unlink_button">
-            <property name="visible">True</property>
-            <property name="action-name">window.unlink-contact</property>
-            <property name="text" translatable="yes">Unlink</property>
-          </object>
-        </child>
-        <child>
-          <object class="GtkSeparator">
-            <property name="visible">True</property>
-          </object>
-        </child>
+      <object class="GtkShortcutController">
+        <property name="scope">global</property>
         <child>
-          <object class="GtkModelButton">
-            <property name="visible">True</property>
-            <property name="action-name">window.delete-contact</property>
-            <property name="text" translatable="yes">Delete</property>
+          <object class="GtkShortcut">
+            <property name="trigger">&lt;Control&gt;n</property>
+            <property name="action">action(window.new-contact)</property>
           </object>
         </child>
       </object>
     </child>
-  </object>
-  <template class="ContactsMainWindow" parent="HdyApplicationWindow">
-    <property name="can_focus">False</property>
-    <property name="default_width">800</property>
-    <property name="default_height">600</property>
-    <property name="icon_name">gnome-contacts</property>
-    <property name="title" translatable="yes">Contacts</property>
-    <signal name="key-press-event" handler="key_press_event_cb" object="ContactsMainWindow" after="yes" 
swapped="no"/>
-    <signal name="delete-event" handler="delete_event_cb" object="ContactsMainWindow" after="no" 
swapped="no"/>
+
     <child>
-      <object class="GtkBox">
-        <property name="visible">True</property>
-        <property name="orientation">vertical</property>
+      <object class="AdwToastOverlay" id="toast_overlay">
         <child>
-          <object class="HdyLeaflet" id="header">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="mode-transition-duration" bind-source="content_box" 
bind-property="mode-transition-duration" bind-flags="bidirectional|sync-create"/>
-            <property name="child-transition-duration" bind-source="content_box" 
bind-property="child-transition-duration" bind-flags="bidirectional|sync-create"/>
-            <property name="transition-type" bind-source="content_box" bind-property="transition-type" 
bind-flags="bidirectional|sync-create"/>
+          <object class="AdwLeaflet" id="content_box">
+            <property name="can-navigate-back">True</property>
+            <signal name="notify::folded" handler="on_folded"/>
+            <signal name="notify::child-transition-running" handler="on_child_transition_running"/>
+
             <child>
-              <object class="HdyHeaderBar" id="left_header">
-                <property name="visible">True</property>
-                <property name="hexpand">False</property>
-                <property name="can_focus">False</property>
-                <property name="title" translatable="yes">Contacts</property>
-                <property name="show_close_button">True</property>
-                <child>
-                  <object class="GtkButton" id="add_button">
-                    <property name="visible">True</property>
-                    <property name="can_focus">True</property>
-                    <property name="halign">center</property>
-                    <property name="valign">center</property>
-                    <property name="tooltip_text" translatable="yes">Create new contact</property>
-                    <signal name="clicked" handler="new_contact" object="ContactsMainWindow" after="no" 
swapped="no"/>
-                    <child internal-child="accessible">
-                      <object class="AtkObject" id="add_button_atkobject">
-                        <property name="AtkObject::accessible-name" translatable="yes">Add contact</property>
-                      </object>
-                    </child>
+              <object class="AdwLeafletPage">
+                <property name="name">list-pane</property>
+                <property name="child">
+                  <object class="GtkBox" id="list_pane_page">
+                    <property name="orientation">vertical</property>
                     <child>
-                      <object class="GtkImage">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="icon_name">list-add-symbolic</property>
-                        <property name="icon_size">1</property>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="pack_type">start</property>
-                  </packing>
-                </child>
+                      <object class="AdwHeaderBar" id="left_header">
+                        <property name="hexpand">False</property>
+                        <property name="show-end-title-buttons" bind-source="content_box" 
bind-property="folded" bind-flags="sync-create"/>
 
-                <child>
-                  <object class="GtkButton" id="selection_button">
-                    <property name="visible">True</property>
-                    <property name="can_focus">True</property>
-                    <property name="halign">center</property>
-                    <property name="valign">center</property>
-                    <property name="tooltip_text" translatable="yes">Select Items</property>
-                    <signal name="clicked" handler="on_selection_button_clicked"/>
-                    <child>
-                      <object class="GtkImage">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="icon_name">object-select-symbolic</property>
-                        <property name="icon_size">1</property>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="pack_type">end</property>
-                    <property name="position">2</property>
-                  </packing>
-                </child>
+                        <child type="start">
+                          <object class="GtkButton" id="add_button">
+                            <property name="tooltip-text" translatable="yes">Create new contact</property>
+                            <property name="icon-name">list-add-symbolic</property>
+                            <property name="action-name">window.new-contact</property>
+                          </object>
+                        </child>
 
-                <child>
-                  <object class="GtkMenuButton" id="hamburger_menu_button">
-                    <property name="visible">True</property>
-                    <property name="can_focus">True</property>
-                    <property name="focus_on_click">False</property>
-                    <property name="popover">hamburger_menu_popover</property>
-                                        <accelerator key="F10" signal="clicked"/>
-                    <property name="tooltip_text" translatable="yes">Menu</property>
-                    <child>
-                      <object class="GtkImage">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="icon_name">open-menu-symbolic</property>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="pack_type">end</property>
-                    <property name="position">1</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkButton" id="select_cancel_button">
-                    <property name="visible">False</property>
-                    <property name="can_focus">True</property>
-                    <property name="focus_on_click">False</property>
-                    <property name="label" translatable="yes">Cancel</property>
-                    <property name="tooltip_text" translatable="yes">Cancel selection</property>
-                  </object>
-                  <packing>
-                    <property name="pack_type">end</property>
-                  </packing>
-                </child>
-              </object>
-              <packing>
-                <property name="name">list-pane</property>
-              </packing>
-            </child>
-            <child>
-              <object class="GtkSeparator" id="header_separator">
-                <property name="visible">True</property>
-                <style>
-                  <class name="sidebar"/>
-                </style>
-              </object>
-              <packing>
-                <property name="navigatable">False</property>
-              </packing>
-            </child>
-            <child>
-              <object class="HdyHeaderBar" id="right_header">
-                <property name="visible">True</property>
-                <property name="hexpand">True</property>
-                <property name="show_close_button">True</property>
-                <child>
-                  <object class="GtkRevealer" id="back_revealer">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="transition-type">slide-right</property>
-                    <property name="transition-duration" bind-source="content_box" 
bind-property="mode-transition-duration" bind-flags="bidirectional|sync-create"/>
-                    <child>
-                      <object class="GtkButton" id="back">
-                        <property name="visible">True</property>
-                        <property name="valign">center</property>
-                        <property name="use-underline">True</property>
-                        <signal name="clicked" handler="on_back_clicked"/>
-                        <style>
-                          <class name="image-button"/>
-                        </style>
-                        <child internal-child="accessible">
-                          <object class="AtkObject" id="a11y-back">
-                            <property name="accessible-name" translatable="yes">Back</property>
+                        <child type="end">
+                          <object class="GtkMenuButton" id="hamburger_menu_button">
+                            <property name="menu-model">hamburger_menu_popover</property>
+                            <property name="primary">True</property>
+                            <property name="tooltip_text" translatable="yes">Menu</property>
+                            <property name="icon-name">open-menu-symbolic</property>
                           </object>
                         </child>
-                        <child>
-                          <object class="GtkImage" id="back_image">
-                            <property name="visible">True</property>
-                            <property name="icon-name">go-previous-symbolic</property>
-                            <property name="icon-size">1</property>
+                        <child type="end">
+                          <object class="GtkButton" id="selection_button">
+                            <property name="icon-name">selection-mode-symbolic</property>
+                            <property name="tooltip-text" translatable="yes">Select Items</property>
+                            <signal name="clicked" handler="on_selection_button_clicked"/>
+                          </object>
+                        </child>
+                        <child type="end">
+                          <object class="GtkButton" id="select_cancel_button">
+                            <property name="visible">False</property>
+                            <property name="label" translatable="yes">Cancel</property>
+                            <property name="tooltip_text" translatable="yes">Cancel Selection</property>
                           </object>
                         </child>
                       </object>
                     </child>
-                  </object>
-                </child>
-                <child>
-                  <object class="GtkButton" id="cancel_button">
-                    <property name="visible">False</property>
-                    <property name="can_focus">True</property>
-                    <property name="focus_on_click">False</property>
-                    <property name="label" translatable="yes">_Cancel</property>
-                    <property name="use_underline">True</property>
-                    <property name="width_request">70</property>
-                    <property name="valign">center</property>
-                    <signal name="notify::visible" handler="on_cancel_visible" object="ContactsMainWindow" 
after="yes" swapped="no"/>
-                    <style>
-                      <class name="text-button"/>
-                    </style>
-                  </object>
-                  <packing>
-                    <property name="pack_type">start</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkToggleButton" id="favorite_button">
-                    <property name="visible">False</property>
-                    <property name="can_focus">True</property>
-                    <property name="focus_on_click">False</property>
-                    <property name="valign">center</property>
-                    <signal name="toggled" handler="on_favorite_button_toggled"/>
-                    <child>
-                      <object class="GtkImage">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="icon_name">starred-symbolic</property>
-                        <property name="icon_size">1</property>
-                      </object>
-                    </child>
-                  </object>
-                  <packing>
-                    <property name="pack_type">end</property>
-                    <property name="position">2</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkMenuButton" id="contact_menu_button">
-                    <property name="visible">False</property>
-                    <property name="can_focus">True</property>
-                    <property name="focus_on_click">False</property>
-                    <property name="popover">contact_sheet_menu</property>
-                    <property name="tooltip_text" translatable="yes">Main Menu</property>
                     <child>
-                      <object class="GtkImage">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="icon_name">view-more-symbolic</property>
+                      <object class="GtkStack" id="list_pane_stack">
+                        <property name="hexpand">False</property>
+                        <child>
+                          <object class="GtkBox">
+                            <property name="orientation">vertical</property>
+                            <property name="width_request">300</property>
+                            <property name="homogeneous">True</property>
+                            <property name="valign">center</property>
+                            <property name="vexpand">True</property>
+                            <child>
+                              <object class="GtkSpinner">
+                                <property name="spinning">True</property>
+                                <property name="valign">end</property>
+                                <property name="halign">center</property>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkLabel" id="label1">
+                                <property name="valign">start</property>
+                                <property name="halign">center</property>
+                                <property name="label" translatable="yes">Loading</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
                       </object>
                     </child>
                   </object>
-                  <packing>
-                    <property name="pack_type">end</property>
-                    <property name="position">1</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkButton" id="done_button">
-                    <property name="visible">False</property>
-                    <property name="can_focus">True</property>
-                    <property name="focus_on_click">False</property>
-                    <property name="label" translatable="yes">Done</property>
-                    <property name="width_request">70</property>
-                    <property name="valign">center</property>
-                    <style>
-                      <class name="text-button"/>
-                    </style>
+                </property>
+              </object>
+            </child>
+
+            <child>
+              <object class="AdwLeafletPage">
+                <property name="navigatable">False</property>
+                <property name="child">
+                  <object class="GtkSeparator">
                   </object>
-                  <packing>
-                    <property name="pack_type">end</property>
-                  </packing>
-                </child>
+                </property>
               </object>
-              <packing>
-                <property name="name">contact-pane</property>
-              </packing>
             </child>
-          </object>
-        </child>
-        <child>
-          <object class="GtkOverlay" id="notification_overlay">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
+
             <child>
-              <object class="HdyLeaflet" id="content_box">
-                <property name="visible">True</property>
-                <property name="can_focus">False</property>
-                <property name="can-swipe-back">True</property>
-                <signal name="notify::folded" handler="on_folded" object="ContactsMainWindow" after="yes" 
swapped="no"/>
-                <signal name="notify::child-transition-running" handler="on_child_transition_running" 
object="ContactsMainWindow" after="yes" swapped="no"/>
-                <child>
-                  <object class="GtkStack" id="list_pane_stack">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="hexpand">False</property>
+              <object class="AdwLeafletPage">
+                <property name="name">contact-pane</property>
+                <property name="child">
+                  <object class="GtkBox" id="contact_pane_page">
+                    <property name="orientation">vertical</property>
                     <child>
-                      <object class="GtkBox">
-                        <property name="visible">True</property>
-                        <property name="can_focus">False</property>
-                        <property name="orientation">vertical</property>
-                        <property name="width_request">300</property>
-                        <property name="homogeneous">True</property>
+                      <object class="AdwHeaderBar" id="right_header">
+                        <property name="hexpand">True</property>
                         <child>
-                          <object class="GtkSpinner">
-                            <property name="visible">True</property>
-                            <property name="can_focus">False</property>
-                            <property name="active">True</property>
-                            <property name="valign">end</property>
-                            <property name="halign">center</property>
-                            <style>
-                              <class name="contacts-watermark"/>
-                            </style>
+                          <object class="GtkRevealer" id="back_revealer">
+                            <property name="transition-type">slide-right</property>
+                            <child>
+                              <object class="GtkButton" id="back">
+                                <property name="valign">center</property>
+                                <property name="icon-name">go-previous-symbolic</property>
+                                <property name="tooltip_text">Back</property>
+                                <signal name="clicked" handler="on_back_clicked"/>
+                              </object>
+                            </child>
                           </object>
                         </child>
+                        <property name="title-widget">
+                          <object class="AdwWindowTitle">
+                            <property name="title"></property>
+                          </object>
+                        </property>
                         <child>
-                          <object class="GtkLabel" id="label1">
-                            <property name="visible">True</property>
-                            <property name="can_focus">False</property>
-                            <property name="valign">start</property>
-                            <property name="halign">center</property>
-                            <property name="label" translatable="yes">Loading</property>
+                          <object class="GtkButton" id="cancel_button">
+                            <property name="visible">False</property>
+                            <property name="label" translatable="yes">_Cancel</property>
+                            <property name="use_underline">True</property>
+                            <signal name="notify::visible" handler="on_cancel_visible" 
object="ContactsMainWindow" after="yes" swapped="no"/>
+                          </object>
+                        </child>
+                        <child type="end">
+                          <object class="GtkBox" id="contact_sheet_buttons">
+                            <property name="visible">False</property>
+                            <property name="orientation">horizontal</property>
+                            <property name="spacing">6</property>
+                            <property name="margin-end">6</property>
+                            <child>
+                              <object class="GtkToggleButton" id="favorite_button">
+                                <property name="icon-name">starred-symbolic</property>
+                                <signal name="toggled" handler="on_favorite_button_toggled"/>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkButton" id="edit_contact_button">
+                                <property name="icon-name">document-edit-symbolic</property>
+                                <property name="action-name">window.edit-contact</property>
+                                <property name="tooltip-text" translatable="yes">Edit Contact</property>
+                              </object>
+                            </child>
+                            <child>
+                              <object class="GtkButton" id="delete_contact_button">
+                                <property name="icon-name">user-trash-symbolic</property>
+                                <property name="action-name">window.delete-contact</property>
+                                <property name="tooltip-text" translatable="yes">Delete Contact</property>
+                              </object>
+                            </child>
+                          </object>
+                        </child>
+                        <child type="end">
+                          <object class="GtkButton" id="done_button">
+                            <property name="visible">False</property>
+                            <property name="use_underline">True</property>
+                            <property name="label" translatable="yes">Done</property>
+                            <property name="valign">center</property>
                             <style>
-                              <class name="contacts-watermark"/>
+                              <class name="suggested-action"/>
                             </style>
                           </object>
                         </child>
                       </object>
                     </child>
+                    <child>
+                      <object class="GtkOverlay" id="contact_pane_container">
+                        <property name="hexpand">True</property>
+                      </object>
+                    </child>
                   </object>
-                  <packing>
-                    <property name="name">list-pane</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkSeparator">
-                    <property name="visible">True</property>
-                    <style>
-                      <class name="sidebar"/>
-                    </style>
-                  </object>
-                  <packing>
-                    <property name="navigatable">False</property>
-                  </packing>
-                </child>
-                <child>
-                  <object class="GtkOverlay" id="contact_pane_container">
-                    <property name="visible">True</property>
-                    <property name="can_focus">False</property>
-                    <property name="hexpand">True</property>
-                  </object>
-                  <packing>
-                    <property name="name">contact-pane</property>
-                  </packing>
-                </child>
+                </property>
               </object>
             </child>
           </object>
@@ -461,32 +242,4 @@
       </object>
     </child>
   </template>
-  <object class="GtkSizeGroup" id="left_pane_size_group">
-    <property name="mode">horizontal</property>
-    <widgets>
-      <widget name="left_header"/>
-      <widget name="list_pane_stack"/>
-    </widgets>
-  </object>
-  <object class="GtkSizeGroup" id="right_pane_size_group">
-    <property name="mode">horizontal</property>
-    <widgets>
-      <widget name="right_header"/>
-      <widget name="contact_pane_container"/>
-    </widgets>
-  </object>
-  <object class="HdyHeaderGroup">
-    <property name="decorate-all" bind-source="content_box" bind-property="folded" bind-flags="sync-create"/>
-    <headerbars>
-      <headerbar name="left_header"/>
-      <headerbar name="right_header"/>
-    </headerbars>
-  </object>
-  <object class="HdySwipeGroup" id="swipe_group">
-    <swipeables>
-      <swipeable name="header"/>
-      <swipeable name="content_box"/>
-    </swipeables>
-  </object>
 </interface>
-
diff --git a/data/ui/contacts-setup-window.ui b/data/ui/contacts-setup-window.ui
index 845c7a0f..6e50592c 100644
--- a/data/ui/contacts-setup-window.ui
+++ b/data/ui/contacts-setup-window.ui
@@ -1,107 +1,49 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <interface>
-  <!-- interface-requires gtk+ 3.22 -->
-  <template class="ContactsSetupWindow" parent="HdyApplicationWindow">
+  <template class="ContactsSetupWindow" parent="AdwApplicationWindow">
     <property name="default_width">800</property>
     <property name="default_height">600</property>
     <child>
       <object class="GtkBox">
-        <property name="visible">True</property>
         <property name="orientation">vertical</property>
         <property name="width_request">360</property>
         <child>
-          <object class="HdyHeaderBar">
-            <property name="visible">True</property>
-            <property name="can_focus">False</property>
-            <property name="title" translatable="yes">Contacts Setup</property>
-            <property name="show_close_button">False</property>
-            <style>
-              <class name="titlebar"/>
-            </style>
+          <object class="GtkHeaderBar">
+            <property name="show-title-buttons">False</property>
+            <property name="title-widget">
+              <object class="AdwWindowTitle">
+                <property name="title" translatable="yes">Contacts Setup</property>
+              </object>
+            </property>
             <child>
               <object class="GtkButton" id="setup_quit_button">
-                <property name="visible">True</property>
-                <property name="can_focus">True</property>
                 <property name="label" translatable="yes">_Quit</property>
                 <property name="use-underline">True</property>
+                <property name="tooltip-text">Cancel Setup And Quit</property>
                 <property name="action_name">app.quit</property>
-                <child internal-child="accessible">
-                  <object class="AtkObject" id="setup_quit_button_atkobject">
-                    <property name="AtkObject::accessible-name" translatable="yes">Cancel setup and 
quit</property>
-                  </object>
-                </child>
               </object>
-              <packing>
-                <property name="pack_type">start</property>
-              </packing>
             </child>
-            <child>
+            <child type="end">
               <object class="GtkButton" id="setup_done_button">
-                <property name="visible">True</property>
                 <property name="sensitive">False</property>
-                <property name="can_focus">True</property>
-                <property name="focus_on_click">False</property>
                 <property name="label" translatable="yes">_Done</property>
+                <property name="tooltip-text" translatable="yes">Complete setup</property>
                 <property name="use-underline">True</property>
-                <child internal-child="accessible">
-                  <object class="AtkObject" id="setup_done_button_atkobject">
-                    <property name="AtkObject::accessible-name" translatable="yes">Setup complete</property>
-                  </object>
-                </child>
                 <style>
-                  <class name="text-button"/>
                   <class name="suggested-action"/>
                 </style>
               </object>
-              <packing>
-                <property name="pack_type">end</property>
-              </packing>
             </child>
           </object>
         </child>
         <child>
-          <object class="GtkScrolledWindow">
-            <property name="visible">True</property>
-            <property name="hscrollbar_policy">never</property>
-            <property name="propagate_natural_height">True</property>
-            <child>
-              <object class="HdyClamp">
-                <property name="visible">True</property>
-                <property name="margin_top">24</property>
-                <property name="margin_bottom">24</property>
-                <property name="margin_start">12</property>
-                <property name="margin_end">12</property>
-                <child>
-                  <object class="GtkBox" id="content">
-                    <property name="visible">True</property>
-                    <property name="valign">center</property>
-                    <property name="halign">center</property>
-                    <property name="spacing">24</property>
-                    <property name="orientation">vertical</property>
-                    <child>
-                      <object class="GtkLabel">
-                        <property name="visible">True</property>
-                        <property name="halign">center</property>
-                        <property name="ellipsize">end</property>
-                        <property name="label" translatable="yes">Welcome</property>
-                        <style>
-                          <class name="large-title"/>
-                        </style>
-                      </object>
-                    </child>
-                    <child>
-                      <object class="GtkLabel">
-                        <property name="visible">True</property>
-                        <property name="halign">start</property>
-                        <property name="wrap">True</property>
-                        <property name="xalign">0</property>
-                        <property name="label" translatable="yes">Please select your main address book: this 
is where new contacts will be added. If you keep your contacts in an online account, you can add them using 
the online accounts settings.</property>
-                      </object>
-                    </child>
-                  </object>
-                </child>
+          <object class="AdwStatusPage">
+            <property name="title" translatable="yes">Welcome</property>
+            <property name="description" translatable="yes">Please select your main address book: this is 
where new contacts will be added. If you keep your contacts in an online account, you can add them using the 
online accounts settings.</property>
+            <property name="child">
+              <object class="AdwClamp" id="clamp">
               </object>
-            </child>
+            </property>
           </object>
         </child>
       </object>
diff --git a/data/ui/style.css b/data/ui/style.css
index 67bd2ad3..47ecbb0e 100644
--- a/data/ui/style.css
+++ b/data/ui/style.css
@@ -7,69 +7,95 @@
   background-color: transparent;
 }
 
-.contacts-suggestion {
-  border-top: 1px solid @borders;
-  background-color: shade(@theme_bg_color, 0.9);
-}
-
-/* Give the avatar in the ContactSheet some margin,
- * so it doesn't jump when switching to the editor. */
-.contacts-contact-sheet .contacts-avatar {
-  margin: 4px 8px;
+/* The search entry to filter the list of contacts */
+.contacts-filter-entry {
+  margin: 6px;
 }
 
-.contacts-postal-entry {
- border-radius: 0 0 0 0;
- border-width: 1px 1px 0 1px;
+/* Draw a little shadow about the contact list when scrolling */
+.contacts-list-scrolled-window undershoot.top {
+  box-shadow: inset 0 1px @borders;
 }
 
-.contacts-postal-entry:nth-child(first) {
- border-radius: 4px 4px 0 0;
-}
-
-.contacts-postal-entry:nth-child(last) {
- border-radius: 0 0 4px 4px;
- border-width: 1px;
+/* The link suggestion widget */
+.contacts-link-suggestion {
+  border-top: 1px solid @borders;
+  background-color: shade(@theme_bg_color, 0.9);
 }
 
-/* The style for the background "watermark" image and text.
- * (copied from dim-label) */
-.contacts-watermark {
-  opacity: 0.55;
-  text-shadow: none;
-}
+  .contacts-link-suggestion avatar {
+    margin: 12px;
+  }
 
 .contacts-avatar-popover .contact-display-name {
   font-size: 20px;
 }
+
 flowboxchild.circular {
   padding: 4px;
   border-radius: 9999px;
-  -gtk-outline-radius: 9999px;
 }
 
-.avatar-button {
-  border-radius: 50%;
-  padding: 0 0;
+/* Contact Sheet/Editor common */
+/* --------------------------- */
+
+.contacts-sheet-container,
+.contacts-contact-editor-container {
+  margin: 32px 36px;
 }
 
-.contacts-flatten:not(:hover) {
-  background-color: transparent;
-  background-image: none;
-  border-color: transparent;
-  box-shadow: inset 0 1px rgba(255, 255, 255, 0), 0 1px rgba(255, 255, 255, 0);
-  text-shadow: none; -gtk-icon-shadow: none;
-  border: 1px solid rgba(205, 199, 194, 0.5);
+.contacts-sheet-property,
+.contacts-editor-property {
+  margin: 9px 6px;
+}
+
+/* Contact Sheet-related CSS class */
+/* ------------------------------- */
+
+.contacts-sheet {
 }
 
-/* remove padding from ListBoxRow so that the revealer doesn't jump */
-row.editor-property-row {
-  padding: 0px;
+.contacts-sheet-header {
+  margin: 0 0 12px 0;
 }
 
-popover list {
-  background-color: @theme_bg_color;
+.contacts-sheet-property {
 }
-popover list row:hover {
-  background-color: @theme_selected_fg_color
+
+
+/* Contact Editor-related CSS classes */
+/* ---------------------------------- */
+
+/* Common class for all widgets editing a property  */
+.contacts-editor-property {
+}
+
+  .contacts-editor-property .contacts-property-icon,
+  .contacts-editor-property .contacts-editor-main-entry image {
+    margin: 12px;
+  }
+
+  .contacts-editor-property .contacts-editor-main-entry {
+    padding: 6px 6px 6px 0; /* left padding is for the icon */
+  }
+
+/* Class for editing postal address */
+.contacts-editor-address entry {
+  border-radius: 0;
+  border-width: 1px 1px 0 1px;
+  padding: 6px 6px;
+}
+
+  .contacts-editor-address entry:first-child {
+    border-radius: 4px 4px 0 0;
+  }
+
+  .contacts-editor-address entry:last-child {
+    border-radius: 0 0 4px 4px;
+    border-width: 1px;
+  }
+
+/* Widget to edit a birthday */
+.contacts-editor-birthday {
+  margin: 12px;
 }
diff --git a/docs/meson.build b/docs/meson.build
index 8ec997d9..8640f4d2 100644
--- a/docs/meson.build
+++ b/docs/meson.build
@@ -13,14 +13,14 @@ custom_target('docs',
     '--pkg=glib-2.0',
     '--pkg=gio-2.0',
     '--pkg=gio-unix-2.0',
-    '--pkg=gtk+-3.0',
+    '--pkg=gtk4',
     '--pkg=gnome-desktop-3.0',
     '--pkg=gee-0.8',
     '--pkg=goa-1.0',
     '--pkg=folks',
     '--pkg=folks-eds',
     '--pkg=libedataserverui-1.2',
-    '--pkg=libhandy-1',
+    '--pkg=libadwaita-1',
     '--pkg=custom',
     '--pkg=config',
     '--directory=@OUTDIR@',
diff --git a/meson.build b/meson.build
index c9b09be3..11813bde 100644
--- a/meson.build
+++ b/meson.build
@@ -47,17 +47,19 @@ gee = dependency('gee-0.8')
 gio_unix = dependency('gio-unix-2.0', version: '>=' + min_glib_version)
 glib = dependency('glib-2.0', version: '>=' + min_glib_version)
 gmodule_export = dependency('gmodule-export-2.0', version: '>=' + min_glib_version)
-gnome_desktop = dependency('gnome-desktop-3.0')
+# gnome_desktop = dependency('gnome-desktop-3.0')
 goa = dependency('goa-1.0')
-gtk = dependency('gtk+-3.0', version: '>= 3.23.1')
-libhandy = dependency('libhandy-1', version: '>= 1.1.0')
+gtk4_dep = dependency('gtk4', version: '>= 4.6')
+libadwaita_dep = dependency('libadwaita-1')
 # E-D-S
 libebook = dependency('libebook-1.2', version: '>=' + min_eds_version)
 libedataserver = dependency('libedataserver-1.2', version: '>=' + min_eds_version)
-libedataserverui = dependency('libedataserverui-1.2', version: '>=' + min_eds_version)
+# libedataserverui = dependency('libedataserverui-1.2', version: '>=' + min_eds_version)
+# Portals (needed for camera access)
+libportal_dep = dependency('libportal', version: '>= 0.5')
 # Cheese
-cheese_dep = dependency('cheese', required: get_option('cheese'))
-cheese_gtk_dep = dependency('cheese-gtk', version: '>= 3.3.91', required: get_option('cheese'))
+# cheese_dep = dependency('cheese', required: get_option('cheese'))
+# cheese_gtk_dep = dependency('cheese-gtk', version: '>= 3.3.91', required: get_option('cheese'))
 
 if get_option('telepathy')
   folks_telepathy = dependency('folks-telepathy', version: '>=' + min_folks_version)
diff --git a/meson_options.txt b/meson_options.txt
index 101161cc..c672bba5 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,5 +1,5 @@
 option('profile', type: 'combo', choices: [ 'default', 'development' ], value: 'default', description: 
'Indicate whether this is a nightly build (used for CI purposes)')
-option('cheese', type: 'feature', value: 'auto', description: 'If enabled, allows creaing an avatar with the 
webcam')
+option('camera', type: 'boolean', value: true, description: 'Support creaing an avatar with the webcam using 
the XDG camera portal')
 option('telepathy', type: 'boolean', value: false, description: 'Enable Telepathy call/chat support.')
 option('manpage', type: 'boolean', value: true, description: 'Enable building man pages.')
 option('docs', type: 'boolean', value: false, description: 'Whether to build the valadoc docs.')
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 2f3c9fae..b23cc38e 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -6,7 +6,7 @@ data/gtk/help-overlay.ui
 data/ui/contacts-accounts-list.ui
 data/ui/contacts-avatar-selector.ui
 data/ui/contacts-contact-pane.ui
-data/ui/contacts-crop-cheese-dialog.ui
+data/ui/contacts-crop-dialog.ui
 data/ui/contacts-editor-menu.ui
 data/ui/contacts-linked-personas-dialog.ui
 data/ui/contacts-link-suggestion-grid.ui
@@ -23,7 +23,7 @@ src/contacts-contact-editor.vala
 src/contacts-contact-list.vala
 src/contacts-contact-pane.vala
 src/contacts-contact-sheet.vala
-src/contacts-crop-cheese-dialog.vala
+src/contacts-crop-dialog.vala
 src/contacts-editor-property.vala
 src/contacts-esd-setup.vala
 src/contacts-im-service.vala
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
index 7348e0eb..26d3f803 100644
--- a/po/POTFILES.skip
+++ b/po/POTFILES.skip
@@ -8,11 +8,11 @@ src/contacts-addressbook-list.c
 src/contacts-app.c
 src/contacts-avatar.c
 src/contacts-avatar-selector.c
+src/contacts-camera-dialog.c
 src/contacts-contact-editor.c
 src/contacts-contact-list.c
 src/contacts-contact-pane.c
 src/contacts-contact-sheet.c
-src/contacts-crop-cheese-dialog.c
 src/contacts-editor-property.c
 src/contacts-esd-setup.c
 src/contacts-im-service.c
diff --git a/src/cc-crop-area.c b/src/cc-crop-area.c
index 799d55c1..e003dffd 100644
--- a/src/cc-crop-area.c
+++ b/src/cc-crop-area.c
@@ -1,5 +1,9 @@
 /*
- * Copyright 2009  Red Hat, Inc,
+ * Copyright 2021  Red Hat, Inc,
+ *
+ * Authors:
+ * - Matthias Clasen <mclasen redhat com>
+ * - Niels De Graef <nielsdg redhat com>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -13,795 +17,687 @@
  *
  * You should have received a copy of the GNU General Public License
  * along with this program; if not, see <http://www.gnu.org/licenses/>.
- *
- * Written by: Matthias Clasen <mclasen redhat com>
  */
 
 #include "config.h"
 
-#include <stdlib.h>
-
 #include <glib.h>
 #include <glib/gi18n.h>
 #include <gtk/gtk.h>
+#include <gsk/gl/gskglrenderer.h>
 
 #include "cc-crop-area.h"
 
-struct _CcCropArea {
-        GtkDrawingArea parent_instance;
-
-        GdkPixbuf *browse_pixbuf;
-        GdkPixbuf *pixbuf;
-        GdkPixbuf *color_shifted;
-        gdouble scale;
-        GdkRectangle image;
-        GdkCursorType current_cursor;
-        GdkRectangle crop;
-        gint active_region;
-        gint last_press_x;
-        gint last_press_y;
-        gint base_width;
-        gint base_height;
-        gdouble aspect;
-};
-
-G_DEFINE_TYPE (CcCropArea, cc_crop_area, GTK_TYPE_DRAWING_AREA);
+/**
+ * CcCropArea:
+ *
+ * A widget that shows a [iface@Gdk.Paintable] and allows the user specify a
+ * cropping rectangle to effectively crop to that given area.
+ */
 
-static inline guchar
-shift_color_byte (guchar b,
-                  int    shift)
-{
-        return CLAMP(b + shift, 0, 255);
-}
+/* Location of the cursor relative to the cropping rectangle/circle */
+typedef enum {
+    OUTSIDE,
+    INSIDE,
+    TOP,
+    TOP_LEFT,
+    TOP_RIGHT,
+    BOTTOM,
+    BOTTOM_LEFT,
+    BOTTOM_RIGHT,
+    LEFT,
+    RIGHT
+} Location;
 
-static void
-shift_colors (GdkPixbuf *pixbuf,
-              gint       red,
-              gint       green,
-              gint       blue,
-              gint       alpha)
-{
-        gint x, y, offset, y_offset, rowstride, width, height;
-        guchar *pixels;
-        gint channels;
-
-        width = gdk_pixbuf_get_width (pixbuf);
-        height = gdk_pixbuf_get_height (pixbuf);
-        rowstride = gdk_pixbuf_get_rowstride (pixbuf);
-        pixels = gdk_pixbuf_get_pixels (pixbuf);
-        channels = gdk_pixbuf_get_n_channels (pixbuf);
-
-        for (y = 0; y < height; y++) {
-                y_offset = y * rowstride;
-                for (x = 0; x < width; x++) {
-                        offset = y_offset + x * channels;
-                        if (red != 0)
-                                pixels[offset] = shift_color_byte (pixels[offset], red);
-                        if (green != 0)
-                                pixels[offset + 1] = shift_color_byte (pixels[offset + 1], green);
-                        if (blue != 0)
-                                pixels[offset + 2] = shift_color_byte (pixels[offset + 2], blue);
-                        if (alpha != 0 && channels >= 4)
-                                pixels[offset + 3] = shift_color_byte (pixels[offset + 3], blue);
-                }
-        }
-}
+struct _CcCropArea {
+    GtkWidget parent_instance;
 
-static void
-update_pixbufs (CcCropArea *area)
-{
-        gint width;
-        gint height;
-        GtkAllocation allocation;
-        gdouble scale;
-        gint dest_width, dest_height;
-        GtkWidget *widget;
-
-        widget = GTK_WIDGET (area);
-        gtk_widget_get_allocation (widget, &allocation);
-
-        width = gdk_pixbuf_get_width (area->browse_pixbuf);
-        height = gdk_pixbuf_get_height (area->browse_pixbuf);
-
-        scale = allocation.height / (gdouble)height;
-        if (scale * width > allocation.width)
-                scale = allocation.width / (gdouble)width;
-
-        dest_width = width * scale;
-        dest_height = height * scale;
-
-        if (area->pixbuf == NULL ||
-            gdk_pixbuf_get_width (area->pixbuf) != allocation.width ||
-            gdk_pixbuf_get_height (area->pixbuf) != allocation.height) {
-                g_clear_object (&area->pixbuf);
-                area->pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB,
-                                                     gdk_pixbuf_get_has_alpha (area->browse_pixbuf),
-                                                     8,
-                                                     dest_width, dest_height);
-                gdk_pixbuf_fill (area->pixbuf, 0x0);
-
-                gdk_pixbuf_scale (area->browse_pixbuf,
-                                  area->pixbuf,
-                                  0, 0,
-                                  dest_width, dest_height,
-                                  0, 0,
-                                  scale, scale,
-                                  GDK_INTERP_BILINEAR);
-
-                g_clear_object (&area->color_shifted);
-                area->color_shifted = gdk_pixbuf_copy (area->pixbuf);
-                shift_colors (area->color_shifted, -100, -100, -100, 0);
-
-                if (area->scale == 0.0) {
-                        gdouble scale_to_80, scale_to_image, crop_scale;
-
-                        /* Scale the crop rectangle to 80% of the area, or less to fit the image */
-                        scale_to_80 = MIN ((gdouble)gdk_pixbuf_get_width (area->pixbuf) * 0.8 / 
area->base_width,
-                                           (gdouble)gdk_pixbuf_get_height (area->pixbuf) * 0.8 / 
area->base_height);
-                        scale_to_image = MIN ((gdouble)dest_width / area->base_width,
-                                              (gdouble)dest_height / area->base_height);
-                        crop_scale = MIN (scale_to_80, scale_to_image);
-
-                        area->crop.width = crop_scale * area->base_width / scale;
-                        area->crop.height = crop_scale * area->base_height / scale;
-                        area->crop.x = (gdk_pixbuf_get_width (area->browse_pixbuf) - area->crop.width) / 2;
-                        area->crop.y = (gdk_pixbuf_get_height (area->browse_pixbuf) - area->crop.height) / 2;
-                }
-
-                area->scale = scale;
-                area->image.x = (allocation.width - dest_width) / 2;
-                area->image.y = (allocation.height - dest_height) / 2;
-                area->image.width = dest_width;
-                area->image.height = dest_height;
-        }
-}
+    GdkPaintable *paintable;
 
-static void
-crop_to_widget (CcCropArea    *area,
-                GdkRectangle  *crop)
-{
-        crop->x = area->image.x + area->crop.x * area->scale;
-        crop->y = area->image.y + area->crop.y * area->scale;
-        crop->width = area->crop.width * area->scale;
-        crop->height = area->crop.height * area->scale;
-}
+    double scale; /* scale factor to go from paintable size to widget size */
 
-typedef enum {
-        OUTSIDE,
-        INSIDE,
-        TOP,
-        TOP_LEFT,
-        TOP_RIGHT,
-        BOTTOM,
-        BOTTOM_LEFT,
-        BOTTOM_RIGHT,
-        LEFT,
-        RIGHT
-} Location;
+    const char *current_cursor;
+    Location active_region;
+    double drag_offx;
+    double drag_offy;
 
-static gboolean
-cc_crop_area_draw (GtkWidget *widget,
-                   cairo_t   *cr)
-{
-        GdkRectangle crop;
-        gint width, height, ix, iy;
-        CcCropArea *uarea = CC_CROP_AREA (widget);
+    /* In source coordinates. See get_scaled_crop() for widget coordinates */
+    GdkRectangle crop;
 
-        if (uarea->browse_pixbuf == NULL)
-                return FALSE;
+    /* In widget coordinates */
+    GdkRectangle image;
+    int min_crop_width;
+    int min_crop_height;
+};
 
-        update_pixbufs (uarea);
+G_DEFINE_TYPE (CcCropArea, cc_crop_area, GTK_TYPE_WIDGET);
 
-        width = gdk_pixbuf_get_width (uarea->pixbuf);
-        height = gdk_pixbuf_get_height (uarea->pixbuf);
-        crop_to_widget (uarea, &crop);
+static void
+update_image_and_crop (CcCropArea *area)
+{
+    GtkAllocation allocation;
+    int width, height;
+    int dest_width, dest_height;
+    double scale;
 
-        ix = uarea->image.x;
-        iy = uarea->image.y;
+    if (area->paintable == NULL)
+        return;
 
-        gdk_cairo_set_source_pixbuf (cr, uarea->color_shifted, ix, iy);
-        cairo_rectangle (cr, ix, iy, width, height);
-        cairo_fill (cr);
+    gtk_widget_get_allocation (GTK_WIDGET (area), &allocation);
 
-        gdk_cairo_set_source_pixbuf (cr, uarea->pixbuf, ix, iy);
-        cairo_arc (cr, crop.x + crop.width / 2, crop.y + crop.width / 2, crop.width / 2, 0, 2 * G_PI);
-        cairo_fill (cr);
+    /* Get the size of the paintable */
+    width = gdk_paintable_get_intrinsic_width (area->paintable);
+    height = gdk_paintable_get_intrinsic_height (area->paintable);
 
-        // draw the four corners
-        cairo_set_source_rgb (cr, 1, 1, 1);
-        cairo_set_line_width (cr, 4.0);
+    /* Find out the scale to convert to widget width/height */
+    scale = allocation.height / (double) height;
+    if (scale * width > allocation.width)
+        scale = allocation.width / (double) width;
 
-        // top left corner
-        cairo_move_to (cr, crop.x + 15, crop.y);
-        cairo_line_to (cr, crop.x, crop.y);
-        cairo_line_to (cr, crop.x, crop.y + 15);
+    dest_width = width * scale;
+    dest_height = height * scale;
 
-        // top right corner
-        cairo_move_to (cr, crop.x + crop.width - 15, crop.y);
-        cairo_line_to (cr, crop.x + crop.width, crop.y);
-        cairo_line_to (cr, crop.x + crop.width, crop.y + 15);
+    if (area->scale == 0.0) {
+        double scale_to_80, scale_to_image, crop_scale;
 
-        // bottom right corner
-        cairo_move_to (cr, crop.x + crop.width - 15, crop.y + crop.height);
-        cairo_line_to (cr, crop.x + crop.width, crop.y + crop.height);
-        cairo_line_to (cr, crop.x + crop.width, crop.y + crop.height - 15);
+        /* Start with a crop area of 80% of the area, unless it's larger than min_size */
+        scale_to_80 = MIN ((double) dest_width * 0.8, (double) dest_height * 0.8);
+        scale_to_image = MIN ((double) area->min_crop_width, (double) area->min_crop_height);
+        crop_scale = MAX (scale_to_80, scale_to_image);
 
-        // bottom left corner
-        cairo_move_to (cr, crop.x + 15, crop.y + crop.height);
-        cairo_line_to (cr, crop.x, crop.y + crop.height);
-        cairo_line_to (cr, crop.x, crop.y + crop.height - 15);
+        /* Divide by `scale` to get back to paintable coordinates */
+        area->crop.width = crop_scale / scale;
+        area->crop.height = crop_scale / scale;
+        area->crop.x = (width - area->crop.width) / 2;
+        area->crop.y = (height - area->crop.height) / 2;
+    }
 
-        cairo_stroke (cr);
+    area->scale = scale;
+    area->image.x = (allocation.width - dest_width) / 2;
+    area->image.y = (allocation.height - dest_height) / 2;
+    area->image.width = dest_width;
+    area->image.height = dest_height;
+}
 
-        return FALSE;
+/* Returns area->crop in widget coordinates (vs paintable coordsinates) */
+static void
+get_scaled_crop (CcCropArea    *area,
+               GdkRectangle  *crop)
+{
+    crop->x = area->image.x + area->crop.x * area->scale;
+    crop->y = area->image.y + area->crop.y * area->scale;
+    crop->width = area->crop.width * area->scale;
+    crop->height = area->crop.height * area->scale;
 }
 
 typedef enum {
-        BELOW,
-        LOWER,
-        BETWEEN,
-        UPPER,
-        ABOVE
+    BELOW,
+    LOWER,
+    BETWEEN,
+    UPPER,
+    ABOVE
 } Range;
 
 static Range
-find_range (gint x,
-            gint min,
-            gint max)
+find_range (int x,
+            int min,
+            int max)
 {
-        gint tolerance = 12;
-
-        if (x < min - tolerance)
-                return BELOW;
-        if (x <= min + tolerance)
-                return LOWER;
-        if (x < max - tolerance)
-                return BETWEEN;
-        if (x <= max + tolerance)
-                return UPPER;
-        return ABOVE;
+    int tolerance = 12;
+
+    if (x < min - tolerance)
+        return BELOW;
+    if (x <= min + tolerance)
+        return LOWER;
+    if (x < max - tolerance)
+        return BETWEEN;
+    if (x <= max + tolerance)
+        return UPPER;
+    return ABOVE;
 }
 
+/* Finds the location of (@x, @y) relative to the crop @rect */
 static Location
 find_location (GdkRectangle *rect,
-               gint          x,
-               gint          y)
+               int           x,
+               int           y)
 {
-        Range x_range, y_range;
-        Location location[5][5] = {
-                { OUTSIDE, OUTSIDE,     OUTSIDE, OUTSIDE,      OUTSIDE },
-                { OUTSIDE, TOP_LEFT,    TOP,     TOP_RIGHT,    OUTSIDE },
-                { OUTSIDE, LEFT,        INSIDE,  RIGHT,        OUTSIDE },
-                { OUTSIDE, BOTTOM_LEFT, BOTTOM,  BOTTOM_RIGHT, OUTSIDE },
-                { OUTSIDE, OUTSIDE,     OUTSIDE, OUTSIDE,      OUTSIDE }
-        };
-
-        x_range = find_range (x, rect->x, rect->x + rect->width);
-        y_range = find_range (y, rect->y, rect->y + rect->height);
-
-        return location[y_range][x_range];
+    Range x_range, y_range;
+    Location location[5][5] = {
+        { OUTSIDE, OUTSIDE,     OUTSIDE, OUTSIDE,      OUTSIDE },
+        { OUTSIDE, TOP_LEFT,    TOP,     TOP_RIGHT,    OUTSIDE },
+        { OUTSIDE, LEFT,        INSIDE,  RIGHT,        OUTSIDE },
+        { OUTSIDE, BOTTOM_LEFT, BOTTOM,  BOTTOM_RIGHT, OUTSIDE },
+        { OUTSIDE, OUTSIDE,     OUTSIDE, OUTSIDE,      OUTSIDE }
+    };
+
+    x_range = find_range (x, rect->x, rect->x + rect->width);
+    y_range = find_range (y, rect->y, rect->y + rect->height);
+
+    return location[y_range][x_range];
 }
 
 static void
 update_cursor (CcCropArea *area,
-               gint           x,
-               gint           y)
+               int         x,
+               int         y)
 {
-        gint cursor_type;
-        GdkRectangle crop;
-        gint region;
-
-        region = area->active_region;
-        if (region == OUTSIDE) {
-                crop_to_widget (area, &crop);
-                region = find_location (&crop, x, y);
-        }
-
-        switch (region) {
-        case OUTSIDE:
-                cursor_type = GDK_LEFT_PTR;
-                break;
-        case TOP_LEFT:
-                cursor_type = GDK_TOP_LEFT_CORNER;
-                break;
-        case TOP:
-                cursor_type = GDK_TOP_SIDE;
-                break;
-        case TOP_RIGHT:
-                cursor_type = GDK_TOP_RIGHT_CORNER;
-                break;
-        case LEFT:
-                cursor_type = GDK_LEFT_SIDE;
-                break;
-        case INSIDE:
-                cursor_type = GDK_FLEUR;
-                break;
-        case RIGHT:
-                cursor_type = GDK_RIGHT_SIDE;
-                break;
-        case BOTTOM_LEFT:
-                cursor_type = GDK_BOTTOM_LEFT_CORNER;
-                break;
-        case BOTTOM:
-                cursor_type = GDK_BOTTOM_SIDE;
-                break;
-        case BOTTOM_RIGHT:
-                cursor_type = GDK_BOTTOM_RIGHT_CORNER;
-                break;
-        default:
-                g_assert_not_reached ();
-        }
-
-        if (cursor_type != area->current_cursor) {
-                GdkCursor *cursor = gdk_cursor_new_for_display (gtk_widget_get_display (GTK_WIDGET (area)),
-                                                                cursor_type);
-                gdk_window_set_cursor (gtk_widget_get_window (GTK_WIDGET (area)), cursor);
-                g_object_unref (cursor);
-                area->current_cursor = cursor_type;
+    const char *cursor_type;
+    GdkRectangle crop;
+    int region;
+
+    region = area->active_region;
+    if (region == OUTSIDE) {
+        get_scaled_crop (area, &crop);
+        region = find_location (&crop, x, y);
+    }
+
+    switch (region) {
+    case OUTSIDE:
+        cursor_type = "default";
+        break;
+    case TOP_LEFT:
+        cursor_type = "nw-resize";
+        break;
+    case TOP:
+        cursor_type = "n-resize";
+        break;
+    case TOP_RIGHT:
+        cursor_type = "ne-resize";
+        break;
+    case LEFT:
+        cursor_type = "w-resize";
+        break;
+    case INSIDE:
+        cursor_type = "move";
+        break;
+    case RIGHT:
+        cursor_type = "e-resize";
+        break;
+    case BOTTOM_LEFT:
+        cursor_type = "sw-resize";
+        break;
+    case BOTTOM:
+        cursor_type = "s-resize";
+        break;
+    case BOTTOM_RIGHT:
+        cursor_type = "se-resize";
+        break;
+    default:
+        g_assert_not_reached ();
+    }
+
+    if (cursor_type != area->current_cursor) {
+        GtkNative *native;
+        g_autoptr (GdkCursor) cursor = NULL;
+
+        native = gtk_widget_get_native (GTK_WIDGET (area));
+        if (!native) {
+            g_warning ("Can't adjust cursor: no GtkNative found");
+            return;
         }
+        cursor = gdk_cursor_new_from_name (cursor_type, NULL);
+        gdk_surface_set_cursor (gtk_native_get_surface (native), cursor);
+        area->current_cursor = cursor_type;
+    }
 }
 
 static int
-eval_radial_line (gdouble center_x, gdouble center_y,
-                  gdouble bounds_x, gdouble bounds_y,
-                  gdouble user_x)
+eval_radial_line (double center_x, double center_y,
+                  double bounds_x, double bounds_y,
+                  double user_x)
 {
-        gdouble decision_slope;
-        gdouble decision_intercept;
+    double decision_slope;
+    double decision_intercept;
 
-        decision_slope = (bounds_y - center_y) / (bounds_x - center_x);
-        decision_intercept = -(decision_slope * bounds_x);
+    decision_slope = (bounds_y - center_y) / (bounds_x - center_x);
+    decision_intercept = -(decision_slope * bounds_x);
 
-        return (int) (decision_slope * user_x + decision_intercept);
+    return (int) (decision_slope * user_x + decision_intercept);
 }
 
 static gboolean
-cc_crop_area_motion_notify_event (GtkWidget      *widget,
-                                  GdkEventMotion *event)
+on_motion (GtkEventControllerMotion *controller,
+           double                    event_x,
+           double                    event_y,
+           void                     *user_data)
 {
-        CcCropArea *area = CC_CROP_AREA (widget);
-        gint x, y;
-        gint delta_x, delta_y;
-        gint width, height;
-        gint adj_width, adj_height;
-        gint pb_width, pb_height;
-        GdkRectangle damage;
-        gint left, right, top, bottom;
-        gdouble new_width, new_height;
-        gdouble center_x, center_y;
-        gint min_width, min_height;
-
-        if (area->browse_pixbuf == NULL)
-                return FALSE;
-
-        update_cursor (area, event->x, event->y);
-
-        crop_to_widget (area, &damage);
-        gtk_widget_queue_draw_area (widget,
-                                    damage.x - 4, damage.y - 4,
-                                    damage.width + 6, damage.height + 6);
-
-        pb_width = gdk_pixbuf_get_width (area->browse_pixbuf);
-        pb_height = gdk_pixbuf_get_height (area->browse_pixbuf);
-
-        x = (event->x - area->image.x) / area->scale;
-        y = (event->y - area->image.y) / area->scale;
-
-        delta_x = x - area->last_press_x;
-        delta_y = y - area->last_press_y;
-        area->last_press_x = x;
-        area->last_press_y = y;
+    CcCropArea *area = CC_CROP_AREA (user_data);
 
-        left = area->crop.x;
-        right = area->crop.x + area->crop.width - 1;
-        top = area->crop.y;
-        bottom = area->crop.y + area->crop.height - 1;
+    if (area->paintable == NULL)
+        return FALSE;
 
-        center_x = (left + right) / 2.0;
-        center_y = (top + bottom) / 2.0;
-
-        switch (area->active_region) {
-        case INSIDE:
-                width = right - left + 1;
-                height = bottom - top + 1;
-
-                left += delta_x;
-                right += delta_x;
-                top += delta_y;
-                bottom += delta_y;
-
-                if (left < 0)
-                        left = 0;
-                if (top < 0)
-                        top = 0;
-                if (right > pb_width)
-                        right = pb_width;
-                if (bottom > pb_height)
-                        bottom = pb_height;
-
-                adj_width = right - left + 1;
-                adj_height = bottom - top + 1;
-                if (adj_width != width) {
-                        if (delta_x < 0)
-                                right = left + width - 1;
-                        else
-                                left = right - width + 1;
-                }
-                if (adj_height != height) {
-                        if (delta_y < 0)
-                                bottom = top + height - 1;
-                        else
-                                top = bottom - height + 1;
-                }
-
-                break;
-
-        case TOP_LEFT:
-                if (area->aspect < 0) {
-                        top = y;
-                        left = x;
-                }
-                else if (y < eval_radial_line (center_x, center_y, left, top, x)) {
-                        top = y;
-                        new_width = (bottom - top) * area->aspect;
-                        left = right - new_width;
-                }
-                else {
-                        left = x;
-                        new_height = (right - left) / area->aspect;
-                        top = bottom - new_height;
-                }
-                break;
-
-        case TOP:
-                top = y;
-                if (area->aspect > 0) {
-                        new_width = (bottom - top) * area->aspect;
-                        right = left + new_width;
-                }
-                break;
-
-        case TOP_RIGHT:
-                if (area->aspect < 0) {
-                        top = y;
-                        right = x;
-                }
-                else if (y < eval_radial_line (center_x, center_y, right, top, x)) {
-                        top = y;
-                        new_width = (bottom - top) * area->aspect;
-                        right = left + new_width;
-                }
-                else {
-                        right = x;
-                        new_height = (right - left) / area->aspect;
-                        top = bottom - new_height;
-                }
-                break;
-
-        case LEFT:
-                left = x;
-                if (area->aspect > 0) {
-                        new_height = (right - left) / area->aspect;
-                        bottom = top + new_height;
-                }
-                break;
-
-        case BOTTOM_LEFT:
-                if (area->aspect < 0) {
-                        bottom = y;
-                        left = x;
-                }
-                else if (y < eval_radial_line (center_x, center_y, left, bottom, x)) {
-                        left = x;
-                        new_height = (right - left) / area->aspect;
-                        bottom = top + new_height;
-                }
-                else {
-                        bottom = y;
-                        new_width = (bottom - top) * area->aspect;
-                        left = right - new_width;
-                }
-                break;
-
-        case RIGHT:
-                right = x;
-                if (area->aspect > 0) {
-                        new_height = (right - left) / area->aspect;
-                        bottom = top + new_height;
-                }
-                break;
-
-        case BOTTOM_RIGHT:
-                if (area->aspect < 0) {
-                        bottom = y;
-                        right = x;
-                }
-                else if (y < eval_radial_line (center_x, center_y, right, bottom, x)) {
-                        right = x;
-                        new_height = (right - left) / area->aspect;
-                        bottom = top + new_height;
-                }
-                else {
-                        bottom = y;
-                        new_width = (bottom - top) * area->aspect;
-                        right = left + new_width;
-                }
-                break;
-
-        case BOTTOM:
-                bottom = y;
-                if (area->aspect > 0) {
-                        new_width = (bottom - top) * area->aspect;
-                        right= left + new_width;
-                }
-                break;
-
-        default:
-                return FALSE;
-        }
+    update_cursor (area, event_x, event_y);
 
-        min_width = area->base_width / area->scale;
-        min_height = area->base_height / area->scale;
+    return FALSE;
+}
 
-        width = right - left + 1;
-        height = bottom - top + 1;
-        if (area->aspect < 0) {
-                if (left < 0)
-                        left = 0;
-                if (top < 0)
-                        top = 0;
-                if (right > pb_width)
-                        right = pb_width;
-                if (bottom > pb_height)
-                        bottom = pb_height;
-
-                width = right - left + 1;
-                height = bottom - top + 1;
-
-                switch (area->active_region) {
-                case LEFT:
-                case TOP_LEFT:
-                case BOTTOM_LEFT:
-                        if (width < min_width)
-                                left = right - min_width;
-                        break;
-                case RIGHT:
-                case TOP_RIGHT:
-                case BOTTOM_RIGHT:
-                        if (width < min_width)
-                                right = left + min_width;
-                        break;
-
-                default: ;
-                }
-
-                switch (area->active_region) {
-                case TOP:
-                case TOP_LEFT:
-                case TOP_RIGHT:
-                        if (height < min_height)
-                                top = bottom - min_height;
-                        break;
-                case BOTTOM:
-                case BOTTOM_LEFT:
-                case BOTTOM_RIGHT:
-                        if (height < min_height)
-                                bottom = top + min_height;
-                        break;
-
-                default: ;
-                }
-        }
-        else {
-                if (left < 0 || top < 0 ||
-                    right > pb_width || bottom > pb_height ||
-                    width < min_width || height < min_height) {
-                        left = area->crop.x;
-                        right = area->crop.x + area->crop.width - 1;
-                        top = area->crop.y;
-                        bottom = area->crop.y + area->crop.height - 1;
-                }
-        }
+static void
+on_drag_begin (GtkGestureDrag *gesture,
+               double          start_x,
+               double          start_y,
+               void           *user_data)
+{
+    CcCropArea *area = CC_CROP_AREA (user_data);
+    GdkRectangle crop;
 
-        area->crop.x = left;
-        area->crop.y = top;
-        area->crop.width = right - left + 1;
-        area->crop.height = bottom - top + 1;
+    if (area->paintable == NULL)
+        return;
 
-        crop_to_widget (area, &damage);
-        gtk_widget_queue_draw_area (widget,
-                                    damage.x - 4, damage.y - 4,
-                                    damage.width + 6, damage.height + 6);
+    update_cursor (area, start_x, start_y);
 
-        return FALSE;
+    get_scaled_crop (area, &crop);
+
+    area->active_region = find_location (&crop, start_x, start_y);
+
+    area->drag_offx = 0.0;
+    area->drag_offy = 0.0;
 }
 
-static gboolean
-cc_crop_area_button_press_event (GtkWidget      *widget,
-                                 GdkEventButton *event)
+static void
+on_drag_update (GtkGestureDrag *gesture,
+                double          offset_x,
+                double          offset_y,
+                void           *user_data)
 {
-        CcCropArea *area = CC_CROP_AREA (widget);
-        GdkRectangle crop;
+    CcCropArea *area = CC_CROP_AREA (user_data);
+    double start_x, start_y;
+    int x, y, delta_x, delta_y;
+    int width, height;
+    int adj_width, adj_height;
+    int pb_width, pb_height;
+    int left, right, top, bottom;
+    double new_width, new_height;
+    double center_x, center_y;
+    int min_width, min_height;
+
+    pb_width = gdk_paintable_get_intrinsic_width (area->paintable);
+    pb_height = gdk_paintable_get_intrinsic_height (area->paintable);
+
+    gtk_gesture_drag_get_start_point (gesture, &start_x, &start_y);
+
+    /* Get the x, y, dx, dy in paintable coords */
+    x = (start_x + offset_x - area->image.x) / area->scale;
+    y = (start_y + offset_y - area->image.y) / area->scale;
+    delta_x = (offset_x - area->drag_offx) / area->scale;
+    delta_y = (offset_y - area->drag_offy) / area->scale;
+
+    /* Helper variables */
+    left = area->crop.x;
+    right = area->crop.x + area->crop.width - 1;
+    top = area->crop.y;
+    bottom = area->crop.y + area->crop.height - 1;
+
+    center_x = (left + right) / 2.0;
+    center_y = (top + bottom) / 2.0;
+
+    /* What we have to do depends on where the user started dragging */
+    switch (area->active_region) {
+    case INSIDE:
+        width = right - left + 1;
+        height = bottom - top + 1;
 
-        if (area->browse_pixbuf == NULL)
-                return FALSE;
+        left = MAX (left + delta_x, 0);
+        right = MIN (right + delta_x, pb_width);
+        top = MAX (top + delta_y, 0);
+        bottom = MIN (bottom + delta_y, pb_height);
+
+        adj_width = right - left + 1;
+        adj_height = bottom - top + 1;
+        if (adj_width != width) {
+            if (delta_x < 0)
+                right = left + width - 1;
+            else
+                left = right - width + 1;
+        }
+        if (adj_height != height) {
+            if (delta_y < 0)
+                bottom = top + height - 1;
+            else
+                top = bottom - height + 1;
+        }
 
-        crop_to_widget (area, &crop);
+        break;
 
-        area->last_press_x = (event->x - area->image.x) / area->scale;
-        area->last_press_y = (event->y - area->image.y) / area->scale;
-        area->active_region = find_location (&crop, event->x, event->y);
+    case TOP_LEFT:
+        if (y < eval_radial_line (center_x, center_y, left, top, x)) {
+            top = y;
+            new_width = bottom - top;
+            left = right - new_width;
+        } else {
+            left = x;
+            new_height = right - left;
+            top = bottom - new_height;
+        }
+        break;
+
+    case TOP:
+        top = y;
+        new_width = bottom - top;
+        right = left + new_width;
+        break;
+
+    case TOP_RIGHT:
+        if (y < eval_radial_line (center_x, center_y, right, top, x)) {
+            top = y;
+            new_width = bottom - top;
+            right = left + new_width;
+        } else {
+            right = x;
+            new_height = right - left;
+            top = bottom - new_height;
+        }
+        break;
+
+    case LEFT:
+        left = x;
+        new_height = right - left;
+        bottom = top + new_height;
+        break;
+
+    case BOTTOM_LEFT:
+        if (y < eval_radial_line (center_x, center_y, left, bottom, x)) {
+            left = x;
+            new_height = right - left;
+            bottom = top + new_height;
+        } else {
+            bottom = y;
+            new_width = bottom - top;
+            left = right - new_width;
+        }
+        break;
+
+    case RIGHT:
+        right = x;
+        new_height = right - left;
+        bottom = top + new_height;
+        break;
+
+    case BOTTOM_RIGHT:
+        if (y < eval_radial_line (center_x, center_y, right, bottom, x)) {
+            right = x;
+            new_height = right - left;
+            bottom = top + new_height;
+        } else {
+            bottom = y;
+            new_width = bottom - top;
+            right = left + new_width;
+        }
+        break;
+
+    case BOTTOM:
+        bottom = y;
+        new_width = bottom - top;
+        right= left + new_width;
+        break;
+
+    default:
+        return;
+    }
+
+    min_width = area->min_crop_width / area->scale;
+    min_height = area->min_crop_height / area->scale;
+
+    width = right - left + 1;
+    height = bottom - top + 1;
+    if (left < 0 || top < 0 ||
+        right > pb_width || bottom > pb_height ||
+        width < min_width || height < min_height) {
+        left = area->crop.x;
+        right = area->crop.x + area->crop.width - 1;
+        top = area->crop.y;
+        bottom = area->crop.y + area->crop.height - 1;
+    }
 
-        gtk_widget_queue_draw_area (widget,
-                                    crop.x - 4, crop.y - 4,
-                                    crop.width + 6, crop.height + 6);
+    area->crop.x = left;
+    area->crop.y = top;
+    area->crop.width = right - left + 1;
+    area->crop.height = bottom - top + 1;
 
-        return FALSE;
+    area->drag_offx = offset_x;
+    area->drag_offy = offset_y;
+
+    gtk_widget_queue_draw (GTK_WIDGET (area));
 }
 
-static gboolean
-cc_crop_area_button_release_event (GtkWidget      *widget,
-                                   GdkEventButton *event)
+static void
+on_drag_end (GtkGestureDrag *gesture,
+             double          offset_x,
+             double          offset_y,
+             void           *user_data)
 {
-        CcCropArea *area = CC_CROP_AREA (widget);
-        GdkRectangle crop;
+    CcCropArea *area = CC_CROP_AREA (user_data);
 
-        if (area->browse_pixbuf == NULL)
-                return FALSE;
-
-        crop_to_widget (area, &crop);
-
-        area->last_press_x = -1;
-        area->last_press_y = -1;
-        area->active_region = OUTSIDE;
+    area->active_region = OUTSIDE;
+    area->drag_offx = 0.0;
+    area->drag_offy = 0.0;
+}
 
-        gtk_widget_queue_draw_area (widget,
-                                    crop.x - 4, crop.y - 4,
-                                    crop.width + 6, crop.height + 6);
+static void
+on_drag_cancel (GtkGesture       *gesture,
+                GdkEventSequence *sequence,
+                void             *user_data)
+{
+    CcCropArea *area = CC_CROP_AREA (user_data);
 
-        return FALSE;
+    area->active_region = OUTSIDE;
+    area->drag_offx = 0;
+    area->drag_offy = 0;
 }
 
 static void
-cc_crop_area_set_size_request (CcCropArea *area)
+cc_crop_area_snapshot (GtkWidget   *widget,
+                       GtkSnapshot *snapshot)
 {
-        gtk_widget_set_size_request (GTK_WIDGET (area),
-                                     area->base_width,
-                                     area->base_height);
+    CcCropArea *area = CC_CROP_AREA (widget);
+    cairo_t *cr;
+    GdkRectangle crop;
+
+    if (area->paintable == NULL)
+        return;
+
+    update_image_and_crop (area);
+
+
+    gtk_snapshot_save (snapshot);
+
+    /* First draw the picture */
+    gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (area->image.x, area->image.y));
+
+    gdk_paintable_snapshot (area->paintable, snapshot, area->image.width, area->image.height);
+
+    /* Draw the cropping UI on top with cairo */
+    cr = gtk_snapshot_append_cairo (snapshot, &GRAPHENE_RECT_INIT (0, 0, area->image.width, 
area->image.height));
+
+    get_scaled_crop (area, &crop);
+    crop.x -= area->image.x;
+    crop.y -= area->image.y;
+
+    /* Draw the circle */
+    cairo_save (cr);
+    cairo_arc (cr, crop.x + crop.width / 2, crop.y + crop.width / 2, crop.width / 2, 0, 2 * G_PI);
+    cairo_rectangle (cr, 0, 0, area->image.width, area->image.height);
+    cairo_set_source_rgba (cr, 0, 0, 0, 0.4);
+    cairo_set_fill_rule (cr, CAIRO_FILL_RULE_EVEN_ODD);
+    cairo_fill (cr);
+    cairo_restore (cr);
+
+    /* draw the four corners */
+    cairo_set_source_rgb (cr, 1, 1, 1);
+    cairo_set_line_width (cr, 4.0);
+
+    /* top left corner */
+    cairo_move_to (cr, crop.x + 15, crop.y);
+    cairo_line_to (cr, crop.x, crop.y);
+    cairo_line_to (cr, crop.x, crop.y + 15);
+    /* top right corner */
+    cairo_move_to (cr, crop.x + crop.width - 15, crop.y);
+    cairo_line_to (cr, crop.x + crop.width, crop.y);
+    cairo_line_to (cr, crop.x + crop.width, crop.y + 15);
+    /* bottom right corner */
+    cairo_move_to (cr, crop.x + crop.width - 15, crop.y + crop.height);
+    cairo_line_to (cr, crop.x + crop.width, crop.y + crop.height);
+    cairo_line_to (cr, crop.x + crop.width, crop.y + crop.height - 15);
+    /* bottom left corner */
+    cairo_move_to (cr, crop.x + 15, crop.y + crop.height);
+    cairo_line_to (cr, crop.x, crop.y + crop.height);
+    cairo_line_to (cr, crop.x, crop.y + crop.height - 15);
+
+    cairo_stroke (cr);
+
+    gtk_snapshot_restore (snapshot);
 }
 
 static void
 cc_crop_area_finalize (GObject *object)
 {
-        CcCropArea *area = CC_CROP_AREA (object);
+    CcCropArea *area = CC_CROP_AREA (object);
 
-        if (area->browse_pixbuf) {
-                g_object_unref (area->browse_pixbuf);
-                area->browse_pixbuf = NULL;
-        }
-        if (area->pixbuf) {
-                g_object_unref (area->pixbuf);
-                area->pixbuf = NULL;
-        }
-        if (area->color_shifted) {
-                g_object_unref (area->color_shifted);
-                area->color_shifted = NULL;
-        }
+    g_clear_object (&area->paintable);
 }
 
 static void
 cc_crop_area_class_init (CcCropAreaClass *klass)
 {
-        GObjectClass   *object_class = G_OBJECT_CLASS (klass);
-        GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
-
-        object_class->finalize = cc_crop_area_finalize;
-        widget_class->draw = cc_crop_area_draw;
-        widget_class->button_press_event = cc_crop_area_button_press_event;
-        widget_class->button_release_event = cc_crop_area_button_release_event;
-        widget_class->motion_notify_event = cc_crop_area_motion_notify_event;
+    GObjectClass *object_class = G_OBJECT_CLASS (klass);
+    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+    object_class->finalize = cc_crop_area_finalize;
+
+    widget_class->snapshot = cc_crop_area_snapshot;
 }
 
 static void
 cc_crop_area_init (CcCropArea *area)
 {
-        gtk_widget_add_events (GTK_WIDGET (area), GDK_POINTER_MOTION_MASK |
-                               GDK_BUTTON_PRESS_MASK |
-                               GDK_BUTTON_RELEASE_MASK);
-
-        area->scale = 0.0;
-        area->image.x = 0;
-        area->image.y = 0;
-        area->image.width = 0;
-        area->image.height = 0;
-        area->active_region = OUTSIDE;
-        area->base_width = 48;
-        area->base_height = 48;
-        area->aspect = 1;
-
-        cc_crop_area_set_size_request (area);
+    GtkGesture *gesture;
+    GtkEventController *controller;
+
+    /* Add handlers for dragging */
+    gesture = gtk_gesture_drag_new ();
+    g_signal_connect (gesture, "drag-begin", G_CALLBACK (on_drag_begin), area);
+    g_signal_connect (gesture, "drag-update", G_CALLBACK (on_drag_update),
+                      area);
+    g_signal_connect (gesture, "drag-end", G_CALLBACK (on_drag_end), area);
+    g_signal_connect (gesture, "cancel", G_CALLBACK (on_drag_cancel), area);
+    gtk_widget_add_controller (GTK_WIDGET (area), GTK_EVENT_CONTROLLER (gesture));
+
+    /* Add handlers for motion events */
+    controller = gtk_event_controller_motion_new ();
+    g_signal_connect (controller, "motion", G_CALLBACK (on_motion), area);
+    gtk_widget_add_controller (GTK_WIDGET (area), GTK_EVENT_CONTROLLER (controller));
+
+    area->scale = 0.0;
+    area->image.x = 0;
+    area->image.y = 0;
+    area->image.width = 0;
+    area->image.height = 0;
+    area->active_region = OUTSIDE;
+    area->min_crop_width = 48;
+    area->min_crop_height = 48;
+
+    gtk_widget_set_size_request (GTK_WIDGET (area), 48, 48);
 }
 
 GtkWidget *
 cc_crop_area_new (void)
 {
-        return g_object_new (CC_TYPE_CROP_AREA, NULL);
+    return g_object_new (CC_TYPE_CROP_AREA, NULL);
 }
 
+/**
+ * cc_crop_area_create_pixbuf:
+ * @area: A crop area
+ *
+ * Renders the area's paintable, with the cropping applied by the user, into a
+ * GdkPixbuf.
+ *
+ * Returns: (transfer full): The cropped picture
+ */
 GdkPixbuf *
-cc_crop_area_get_picture (CcCropArea *area)
+cc_crop_area_create_pixbuf (CcCropArea *area)
 {
-        gint width, height;
+    g_autoptr (GtkSnapshot) snapshot = NULL;
+    g_autoptr (GskRenderNode) node = NULL;
+    g_autoptr (GskRenderer) renderer = NULL;
+    g_autoptr (GdkTexture) texture = NULL;
+    g_autoptr (GError) error = NULL;
+    graphene_rect_t viewport;
+
+    g_return_val_if_fail (CC_IS_CROP_AREA (area), NULL);
+
+    snapshot = gtk_snapshot_new ();
+    gdk_paintable_snapshot (area->paintable, snapshot,
+                            gdk_paintable_get_intrinsic_width (area->paintable),
+                            gdk_paintable_get_intrinsic_height (area->paintable));
+    node = gtk_snapshot_free_to_node (g_steal_pointer (&snapshot));
+
+    renderer = gsk_gl_renderer_new ();
+    if (!gsk_renderer_realize (renderer, NULL, &error)) {
+        g_warning ("Couldn't realize GL renderer: %s", error->message);
+        return NULL;
+    }
+    viewport = GRAPHENE_RECT_INIT (area->crop.x, area->crop.y,
+                                   area->crop.width, area->crop.height);
+    texture = gsk_renderer_render_texture (renderer, node, &viewport);
+    gsk_renderer_unrealize (renderer);
+
+    return gdk_pixbuf_get_from_texture (texture);
+}
 
-        width = gdk_pixbuf_get_width (area->browse_pixbuf);
-        height = gdk_pixbuf_get_height (area->browse_pixbuf);
-        width = MIN (area->crop.width, width - area->crop.x);
-        height = MIN (area->crop.height, height - area->crop.y);
+/**
+ * cc_crop_area_get_paintable:
+ * @area: A crop area
+ *
+ * Returns the area's paintable, unmodified.
+ *
+ * Returns: (transfer none) (nullable): The paintable which the user can crop
+ */
+GdkPaintable *
+cc_crop_area_get_paintable (CcCropArea *area)
+{
+    g_return_val_if_fail (CC_IS_CROP_AREA (area), NULL);
 
-        return gdk_pixbuf_new_subpixbuf (area->browse_pixbuf,
-                                         area->crop.x,
-                                         area->crop.y,
-                                         width, height);
+    return area->paintable;
 }
 
 void
-cc_crop_area_set_picture (CcCropArea *area,
-                          GdkPixbuf  *pixbuf)
+cc_crop_area_set_paintable (CcCropArea   *area,
+                            GdkPaintable *paintable)
 {
-        int width;
-        int height;
-
-        if (area->browse_pixbuf) {
-                g_object_unref (area->browse_pixbuf);
-                area->browse_pixbuf = NULL;
-        }
-        if (pixbuf) {
-                area->browse_pixbuf = g_object_ref (pixbuf);
-                width = gdk_pixbuf_get_width (pixbuf);
-                height = gdk_pixbuf_get_height (pixbuf);
-        } else {
-                width = 0;
-                height = 0;
-        }
+    g_return_if_fail (CC_IS_CROP_AREA (area));
+    g_return_if_fail (GDK_IS_PAINTABLE (paintable));
 
-        area->crop.width = 2 * area->base_width;
-        area->crop.height = 2 * area->base_height;
-        area->crop.x = (width - area->crop.width) / 2;
-        area->crop.y = (height - area->crop.height) / 2;
+    g_set_object (&area->paintable, paintable);
 
-        area->scale = 0.0;
-        area->image.x = 0;
-        area->image.y = 0;
-        area->image.width = 0;
-        area->image.height = 0;
+    area->scale = 0.0;
+    area->image.x = 0;
+    area->image.y = 0;
+    area->image.width = 0;
+    area->image.height = 0;
 
-        gtk_widget_queue_draw (GTK_WIDGET (area));
+    gtk_widget_queue_draw (GTK_WIDGET (area));
 }
 
+/**
+ * cc_crop_area_set_min_size:
+ * @area: A crop widget
+ * @width: The minimal width
+ * @height: The minimal height
+ *
+ * Sets the minimal size of the crop rectangle (in paintable coordinates)
+ */
 void
 cc_crop_area_set_min_size (CcCropArea *area,
-                           gint        width,
-                           gint        height)
+                           int         width,
+                           int         height)
 {
-        area->base_width = width;
-        area->base_height = height;
-
-        cc_crop_area_set_size_request (area);
+    g_return_if_fail (CC_IS_CROP_AREA (area));
 
-        if (area->aspect > 0) {
-                area->aspect = area->base_width / (gdouble)area->base_height;
-        }
-}
+    area->min_crop_width = width;
+    area->min_crop_height = height;
 
-void
-cc_crop_area_set_constrain_aspect (CcCropArea *area,
-                                   gboolean    constrain)
-{
-        if (constrain) {
-                area->aspect = area->base_width / (gdouble)area->base_height;
-        }
-        else {
-                area->aspect = -1;
-        }
+    gtk_widget_set_size_request (GTK_WIDGET (area),
+                                 area->min_crop_width,
+                                 area->min_crop_height);
 }
-
diff --git a/src/cc-crop-area.h b/src/cc-crop-area.h
index 1cc1788b..da80024a 100644
--- a/src/cc-crop-area.h
+++ b/src/cc-crop-area.h
@@ -26,17 +26,16 @@
 G_BEGIN_DECLS
 
 #define CC_TYPE_CROP_AREA (cc_crop_area_get_type ())
-G_DECLARE_FINAL_TYPE (CcCropArea, cc_crop_area, CC, CROP_AREA, GtkDrawingArea)
+G_DECLARE_FINAL_TYPE (CcCropArea, cc_crop_area, CC, CROP_AREA, GtkWidget)
 
-GtkWidget *cc_crop_area_new                  (void);
-GdkPixbuf *cc_crop_area_get_picture          (CcCropArea *area);
-void       cc_crop_area_set_picture          (CcCropArea *area,
-                                              GdkPixbuf  *pixbuf);
-void       cc_crop_area_set_min_size         (CcCropArea *area,
-                                              gint        width,
-                                              gint        height);
-void       cc_crop_area_set_constrain_aspect (CcCropArea *area,
-                                              gboolean    constrain);
+GtkWidget *      cc_crop_area_new                  (void);
+GdkPaintable *   cc_crop_area_get_paintable        (CcCropArea   *area);
+void             cc_crop_area_set_paintable        (CcCropArea   *area,
+                                                    GdkPaintable *paintable);
+void             cc_crop_area_set_min_size         (CcCropArea   *area,
+                                                    int           width,
+                                                    int           height);
+GdkPixbuf *      cc_crop_area_create_pixbuf        (CcCropArea   *area);
 
 G_END_DECLS
 
diff --git a/src/contacts-accounts-list.vala b/src/contacts-accounts-list.vala
index 9690e917..8be6a1ab 100644
--- a/src/contacts-accounts-list.vala
+++ b/src/contacts-accounts-list.vala
@@ -18,26 +18,31 @@
 using Folks;
 
 [GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-accounts-list.ui")]
-public class Contacts.AccountsList : Gtk.ListBox {
-  private Gtk.ListBoxRow last_selected_row;
+public class Contacts.AccountsList : Adw.Bin {
 
-  private Store contacts_store;
+  [GtkChild]
+  private unowned Gtk.ListBox listbox;
+  private unowned Gtk.ListBoxRow? last_selected_row = null;
 
-  public PersonaStore? selected_store;
+  private Store contacts_store;
+  public PersonaStore? selected_store = null;
 
   public signal void account_selected ();
 
+  construct {
+    this.listbox.row_activated.connect (on_row_activated);
+  }
+
   public AccountsList (Store contacts_store) {
     this.contacts_store = contacts_store;
-    this.selected_store = null;
   }
 
-  public override void row_activated (Gtk.ListBoxRow row) {
+  private void on_row_activated (Gtk.ListBox listbox, Gtk.ListBoxRow? row) {
     if (row == null)
       return;
 
-    if (last_selected_row != null &&
-        last_selected_row == row) {
+    if (this.last_selected_row != null &&
+        this.last_selected_row == row) {
       return;
     }
 
@@ -45,29 +50,33 @@ public class Contacts.AccountsList : Gtk.ListBox {
     checkmark.show ();
 
     if (last_selected_row != null) {
-      checkmark = last_selected_row.get_data<Gtk.Image> ("checkmark");
+      checkmark = this.last_selected_row.get_data<Gtk.Image> ("checkmark");
       if (checkmark != null)
         checkmark.hide ();
     }
 
-    last_selected_row = row;
-
-    selected_store = row.get_data<PersonaStore> ("store");
+    // Update the fields
+    this.last_selected_row = row;
+    this.selected_store = row.get_data<PersonaStore> ("store");
 
     account_selected ();
   }
 
   public void update_contents (bool select_active) {
-    foreach (var child in get_children ()) {
-      child.destroy ();
+    // Remove all entries
+    unowned var child = this.listbox.get_first_child ();
+    while (child != null) {
+      unowned var next = child.get_next_sibling ();
+      this.listbox.remove (child);
+      child = next;
     }
 
     // Fill the list with address book
     PersonaStore[] eds_stores = Utils.get_eds_address_books (this.contacts_store);
     debug ("Found %d EDS stores", eds_stores.length);
 
-    PersonaStore? local_store = null;
-    foreach (var persona_store in eds_stores) {
+    unowned PersonaStore? local_store = null;
+    foreach (unowned var persona_store in eds_stores) {
       if (persona_store.id == "system-address-book") {
         local_store = persona_store;
         continue;
@@ -85,63 +94,60 @@ public class Contacts.AccountsList : Gtk.ListBox {
         source_account_id = goa_source_ext.account_id;
       }
 
-      var row = new Hdy.ActionRow ();
+      var row = new Adw.ActionRow ();
       row.set_data ("store", persona_store);
 
       Gtk.Image provider_image;
       if (source_account_id != "")
         provider_image = Contacts.get_icon_for_goa_account (source_account_id);
       else
-        provider_image = new Gtk.Image.from_icon_name (Config.APP_ID,
-                                                       Gtk.IconSize.DIALOG);
+        provider_image = new Gtk.Image.from_icon_name (Config.APP_ID);
+      provider_image.icon_size = Gtk.IconSize.LARGE;
       row.add_prefix (provider_image);
       row.title = provider_name;
       row.subtitle = parent_source.display_name;
-      row.show_all ();
-      row.no_show_all = true;
-      var checkmark = new Gtk.Image.from_icon_name ("object-select-symbolic",
-                                                    Gtk.IconSize.MENU);
-      checkmark.set ("margin-end", 6,
-                     "valign", Gtk.Align.CENTER,
-                     "halign", Gtk.Align.END,
-                     "vexpand", true,
-                     "hexpand", true);
-      row.add (checkmark);
+
+      var checkmark = new Gtk.Image.from_icon_name ("object-select-symbolic");
+      checkmark.margin_end = 6;
+      checkmark.valign = Gtk.Align.CENTER;
+      checkmark.halign = Gtk.Align.END;
+      checkmark.hexpand = true;
+      checkmark.vexpand = true;
+      checkmark.visible = (persona_store == this.contacts_store.aggregator.primary_store);
+      row.add_suffix (checkmark);
       row.set_activatable_widget (checkmark);
       row.set_data ("checkmark", checkmark);
-      add (row);
+
+      this.listbox.append (row);
 
       if (select_active &&
           persona_store == this.contacts_store.aggregator.primary_store) {
-        row_activated (row);
+        this.listbox.row_activated (row);
       }
     }
 
     if (local_store != null) {
-      var local_row = new Hdy.ActionRow ();
-      var provider_image = new Gtk.Image.from_icon_name (Config.APP_ID,
-                                                         Gtk.IconSize.DIALOG);
+      var local_row = new Adw.ActionRow ();
+      var provider_image = new Gtk.Image.from_icon_name (Config.APP_ID);
+      provider_image.icon_size = Gtk.IconSize.LARGE;
       local_row.add_prefix (provider_image);
       local_row.title = _("Local Address Book");
-      local_row.show_all ();
-      local_row.no_show_all = true;
-      var checkmark = new Gtk.Image.from_icon_name ("object-select-symbolic", Gtk.IconSize.MENU);
-      checkmark.set ("margin-end", 6,
-                     "valign", Gtk.Align.CENTER,
-                     "halign", Gtk.Align.END,
-                     "vexpand", true,
-                     "hexpand", true);
-      local_row.add (checkmark);
+      var checkmark = new Gtk.Image.from_icon_name ("object-select-symbolic");
+      checkmark.margin_end = 6;
+      checkmark.valign = Gtk.Align.CENTER;
+      checkmark.halign = Gtk.Align.END;
+      checkmark.hexpand = true;
+      checkmark.vexpand = true;
+      checkmark.visible = (local_store == this.contacts_store.aggregator.primary_store);
+      local_row.add_suffix (checkmark);
       local_row.set_activatable_widget (checkmark);
       local_row.set_data ("checkmark", checkmark);
       local_row.set_data ("store", local_store);
-      add (local_row);
+      this.listbox.append (local_row);
       if (select_active &&
           local_store == this.contacts_store.aggregator.primary_store) {
-        row_activated (local_row);
+        this.listbox.row_activated (local_row);
       }
     }
-
-    show_all ();
   }
 }
diff --git a/src/contacts-addressbook-dialog.vala b/src/contacts-addressbook-dialog.vala
index f9cc8275..ab0fb926 100644
--- a/src/contacts-addressbook-dialog.vala
+++ b/src/contacts-addressbook-dialog.vala
@@ -31,36 +31,34 @@ public class Contacts.AddressbookDialog : Gtk.Dialog {
     add_buttons (_("Change"), Gtk.ResponseType.OK,
                  _("Cancel"), Gtk.ResponseType.CANCEL);
 
-    var content_area = get_content_area () as Gtk.Box;
-    content_area.border_width = 0;
-
     var ok_button = get_widget_for_response (Gtk.ResponseType.OK);
     ok_button.sensitive = false;
     ok_button.get_style_context ().add_class ("suggested-action");
 
-    var scrolled_window = new Gtk.ScrolledWindow (null, null);
-    scrolled_window.expand = true;
+    var scrolled_window = new Gtk.ScrolledWindow ();
+    scrolled_window.hexpand = true;
+    scrolled_window.vexpand = true;
     scrolled_window.height_request = 300;
     scrolled_window.hscrollbar_policy = Gtk.PolicyType.NEVER;
     scrolled_window.propagate_natural_height = true;
-    content_area.add (scrolled_window);
+    ((Gtk.Box) this.get_content_area ()).append (scrolled_window);
 
-    var clamp = new Hdy.Clamp ();
+    var clamp = new Adw.Clamp ();
     clamp.margin_top = 32;
     clamp.margin_bottom = 32;
     clamp.margin_start = 12;
     clamp.margin_end = 12;
     clamp.maximum_size = 400;
-    scrolled_window.add (clamp);
+    scrolled_window.set_child (clamp);
 
     var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 12);
     box.valign = Gtk.Align.START;
-    clamp.add (box);
+    clamp.set_child (box);
 
     var explanation_label = new Gtk.Label (_("New contacts will be added to the selected address book.\nYou 
are able to view and edit contacts from other address books."));
     explanation_label.xalign = 0;
     explanation_label.wrap = true;
-    box.add (explanation_label);
+    box.append (explanation_label);
 
     this.accounts_list = new AccountsList (contacts_store);
     this.accounts_list.update_contents (true);
@@ -72,12 +70,10 @@ public class Contacts.AddressbookDialog : Gtk.Dialog {
     });
 
     contacts_store.backend_store.backend_available.connect (() => {
-        this.accounts_list.update_contents (true);
+      this.accounts_list.update_contents (true);
     });
 
-    box.add (this.accounts_list);
-
-    show_all ();
+    box.append (this.accounts_list);
   }
 
   public override void response (int response) {
@@ -88,7 +84,7 @@ public class Contacts.AddressbookDialog : Gtk.Dialog {
     if (e_store != null) {
       eds_source_registry.set_default_address_book (e_store.source);
       var settings = new GLib.Settings ("org.freedesktop.folks");
-      settings.set_string ("primary-store", "eds:%s".printf(e_store.id));
+      settings.set_string ("primary-store", "eds:%s".printf (e_store.id));
     }
   }
 }
diff --git a/src/contacts-addressbook-list.vala b/src/contacts-addressbook-list.vala
index a7b26167..a5d2e8bb 100644
--- a/src/contacts-addressbook-list.vala
+++ b/src/contacts-addressbook-list.vala
@@ -19,18 +19,28 @@
 
 using Folks;
 
-public class Contacts.AddressbookList : Gtk.ListBox {
+public class Contacts.AddressbookList : Adw.Bin {
+
   private BackendStore store;
-  private AddressbookRow? marked_row;
   private bool show_icon;
 
+  private unowned Gtk.ListBox listbox;
+  private AddressbookRow? marked_row = null;
+
   public signal void addressbook_selected ();
 
+  construct {
+    var list_box = new Gtk.ListBox ();
+    list_box.row_activated.connect (on_row_activated);
+    list_box.set_header_func (list_box_update_header_func);
+    this.listbox = list_box;
+    this.child = this.listbox;
+  }
+
   public AddressbookList (BackendStore store, bool icon = true) {
     this.store = store;
     this.show_icon = icon;
 
-    this.set_header_func (list_box_update_header_func);
     this.update ();
   }
 
@@ -44,7 +54,7 @@ public class Contacts.AddressbookList : Gtk.ListBox {
     }
   }
 
-  public override void row_activated (Gtk.ListBoxRow row) {
+  private void on_row_activated (Gtk.ListBox listbox, Gtk.ListBoxRow row) {
     var addressbook = row as AddressbookRow;
     if (addressbook == null)
       return;
@@ -62,8 +72,12 @@ public class Contacts.AddressbookList : Gtk.ListBox {
   }
 
   public void update () {
-    foreach (var child in get_children ()) {
-      child.destroy ();
+    // Remove all entries
+    unowned var child = this.listbox.get_first_child ();
+    while (child != null) {
+      unowned var next = child.get_next_sibling ();
+      this.listbox.remove (child);
+      child = next;
     }
 
     // Fill the list with address book
@@ -93,25 +107,24 @@ public class Contacts.AddressbookList : Gtk.ListBox {
         if (source_account_id != "")
           provider_image = Contacts.get_icon_for_goa_account (source_account_id);
         else
-          provider_image = new Gtk.Image.from_icon_name (Config.APP_ID, Gtk.IconSize.DIALOG);
+          provider_image = new Gtk.Image.from_icon_name (Config.APP_ID);
       }
 
       var row = new AddressbookRow (provider_name, parent_source.display_name, provider_image);
-      add (row);
+      this.listbox.append (row);
     }
 
     if (local_store != null) {
-      var provider_image = this.show_icon? new Gtk.Image.from_icon_name (Config.APP_ID, Gtk.IconSize.DIALOG) 
: null;
+      var provider_image = this.show_icon? new Gtk.Image.from_icon_name (Config.APP_ID) : null;
       var local_row = new AddressbookRow (_("Local Address Book"), null, provider_image);
-      add (local_row);
+      this.listbox.append (local_row);
     }
-
-    show_all ();
   }
 }
 
-public class Contacts.AddressbookRow : Hdy.ActionRow {
+public class Contacts.AddressbookRow : Adw.ActionRow {
   Gtk.Widget checkmark;
+
   public AddressbookRow (string title, string? subtitle, Gtk.Widget? image = null) {
     this.set_selectable (false);
     if (image != null) {
@@ -121,15 +134,13 @@ public class Contacts.AddressbookRow : Hdy.ActionRow {
     if (subtitle != null) {
       this.subtitle = subtitle;
     }
-    this.show_all ();
-    this.no_show_all = true;
-    this.checkmark = new Gtk.Image.from_icon_name ("object-select-symbolic", Gtk.IconSize.MENU);
+    this.checkmark = new Gtk.Image.from_icon_name ("object-select-symbolic");
     this.checkmark.set ("margin-end", 6,
                         "valign", Gtk.Align.CENTER,
                         "halign", Gtk.Align.END,
                         "vexpand", true,
                         "hexpand", true);
-    this.add (this.checkmark);
+    this.set_child (this.checkmark);
   }
 
   public void unselect () {
diff --git a/src/contacts-app.vala b/src/contacts-app.vala
index b1adaf40..c746aadd 100644
--- a/src/contacts-app.vala
+++ b/src/contacts-app.vala
@@ -17,12 +17,12 @@
 
 using Folks;
 
-public class Contacts.App : Gtk.Application {
+public class Contacts.App : Adw.Application {
   private Settings settings;
 
   private Store contacts_store;
 
-  private MainWindow window;
+  private unowned MainWindow window;
 
   private const GLib.ActionEntry[] action_entries = {
     { "quit",             quit                },
@@ -30,7 +30,6 @@ public class Contacts.App : Gtk.Application {
     { "about",            show_about          },
     { "change-book",      change_address_book },
     { "online-accounts",  online_accounts     },
-    { "new-contact",      new_contact         },
     { "show-contact",     on_show_contact, "s"}
   };
 
@@ -45,6 +44,7 @@ public class Contacts.App : Gtk.Application {
   public App () {
     Object (
       application_id: Config.APP_ID,
+      resource_base_path: "/org/gnome/Contacts",
       flags: ApplicationFlags.HANDLES_COMMAND_LINE
     );
 
@@ -84,14 +84,14 @@ public class Contacts.App : Gtk.Application {
   }
 
   public void show_contact (Individual? individual) {
-    window.set_shown_contact (individual);
+    this.window.set_shown_contact (individual);
   }
 
   public async void show_individual (string id) {
     if (contacts_store.is_quiescent) {
       show_individual_ready.begin (id);
     } else {
-      contacts_store.quiescent.connect( () => {
+      contacts_store.quiescent.connect (() => {
         show_individual_ready.begin (id);
       });
     }
@@ -111,15 +111,15 @@ public class Contacts.App : Gtk.Application {
                                           Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE,
                                           _("No contact with id %s found"), id);
       dialog.set_title (_("Contact not found"));
-      dialog.run ();
-      dialog.destroy ();
+      dialog.response.connect ((_) => { dialog.close (); });
+      dialog.show ();
     }
   }
 
   public void change_address_book () {
     var dialog = new AddressbookDialog (this.contacts_store, this.window);
-    dialog.run ();
-    dialog.destroy ();
+    dialog.response.connect ((_) => dialog.close ());
+    dialog.show ();
   }
 
   public void online_accounts () {
@@ -131,7 +131,7 @@ public class Contacts.App : Gtk.Application {
                                               "/org/gnome/ControlCenter",
                                               "org.gtk.Actions");
 
-      var builder = new VariantBuilder (new VariantType ("av") );
+      var builder = new VariantBuilder (new VariantType ("av"));
       builder.add ("v", new Variant.string (""));
       var param = new Variant.tuple ({
         new Variant.string ("launch-panel"),
@@ -149,11 +149,8 @@ public class Contacts.App : Gtk.Application {
   }
 
   public void show_help () {
-    try {
-      Gtk.show_uri_on_window (window, "help:gnome-help/contacts", Gtk.get_current_event_time ());
-    } catch (GLib.Error e1) {
-      warning ("Error showing help: %s", e1.message);
-    }
+    // FIXME: use show_uri_full(), so we can report errors
+    Gtk.show_uri (this.window, "help:gnome-help/contacts", Gdk.CURRENT_TIME);
   }
 
   public void show_about () {
@@ -166,11 +163,10 @@ public class Contacts.App : Gtk.Application {
     string[] artists = {
       "Allan Day <allanpday gmail com>"
     };
-    Gtk.show_about_dialog (window,
+    Gtk.show_about_dialog (this.window,
                            "artists", artists,
                            "authors", authors,
                            "translator-credits", _("translator-credits"),
-                           "program-name", _("GNOME Contacts"),
                            "title", _("About GNOME Contacts"),
                            "comments", _("Contact Management Application"),
                            "copyright", _("© 2011 Red Hat, Inc.\n© 2011-2020 The Contacts Developers"),
@@ -182,7 +178,7 @@ public class Contacts.App : Gtk.Application {
   }
 
   public async void show_by_email (string email_address) {
-    var query = new SimpleQuery(email_address, { "email-addresses" });
+    var query = new SimpleQuery (email_address, { "email-addresses" });
     Individual individual = yield contacts_store.find_contact (query);
     if (individual != null) {
       show_contact (individual);
@@ -191,23 +187,25 @@ public class Contacts.App : Gtk.Application {
                                           Gtk.MessageType.ERROR, Gtk.ButtonsType.CLOSE,
                                           _("No contact with email address %s found"), email_address);
       dialog.set_title (_("Contact not found"));
-      dialog.run ();
-      dialog.destroy ();
+      dialog.response.connect ((_) => dialog.close ());
+      dialog.show ();
     }
   }
 
   public void show_search (string query) {
     if (contacts_store.is_quiescent) {
-      window.show_search (query);
+      this.window.show_search (query);
     } else {
       contacts_store.quiescent.connect_after (() => {
-        window.show_search (query);
+        this.window.show_search (query);
       });
     }
   }
 
   private void create_window () {
-    this.window = new MainWindow (this.settings, this, this.contacts_store);
+    var win = new MainWindow (this.settings, this, this.contacts_store);
+    win.show ();
+    this.window = win;
 
     show_contact_list ();
   }
@@ -244,8 +242,6 @@ public class Contacts.App : Gtk.Application {
     this.contacts_store = new Store ();
     base.startup ();
 
-    Hdy.init ();
-
     load_styling ();
     create_actions ();
   }
@@ -254,16 +250,15 @@ public class Contacts.App : Gtk.Application {
     this.add_action_entries (action_entries, this);
 
     this.set_accels_for_action ("app.help", {"F1"});
-    this.set_accels_for_action ("app.new-contact", {"<Primary>n"});
-    this.set_accels_for_action ("win.show-help-overlay", {"<Primary>question"});
+    this.set_accels_for_action ("app.quit", {"<Control>q"});
   }
 
   public void load_styling () {
     var provider = new Gtk.CssProvider ();
     provider.load_from_resource ("/org/gnome/Contacts/ui/style.css");
-    Gtk.StyleContext.add_provider_for_screen (Gdk.Screen.get_default(),
-                                              provider,
-                                              Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
+    Gtk.StyleContext.add_provider_for_display (Gdk.Display.get_default (),
+                                               provider,
+                                               Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
   }
 
   public override void activate () {
@@ -275,28 +270,26 @@ public class Contacts.App : Gtk.Application {
   }
 
   private void run_setup () {
-    // Disable the change-book action (don't want the user to do that during setup)
-    var change_book_action = lookup_action ("change-book") as SimpleAction;
+    debug ("Running initial setup");
+
+    // Disable change-book action (don't want the user to do that during setup)
+    unowned var change_book_action = lookup_action ("change-book") as SimpleAction;
     change_book_action.set_enabled (false);
 
     // Create and show the setup window
     var setup_window = new SetupWindow (this, this.contacts_store);
-    setup_window.setup_done.connect ( (selected_store) => {
-        setup_window.destroy ();
+    setup_window.setup_done.connect ((selected_store) => {
+      setup_window.destroy ();
 
-        eds_source_registry.set_default_address_book (selected_store.source);
-        this.settings.did_initial_setup = true;
+      eds_source_registry.set_default_address_book (selected_store.source);
+      this.settings.did_initial_setup = true;
 
-        change_book_action.set_enabled (true); // re-enable change-book action
-        create_window ();
-      });
+      change_book_action.set_enabled (true);   // re-enable change-book action
+      create_window ();
+    });
     setup_window.show ();
   }
 
-  public void new_contact () {
-    this.window.new_contact ();
-  }
-
   private void on_show_contact(SimpleAction action, Variant? param) {
     activate();
 
diff --git a/src/contacts-avatar-selector.vala b/src/contacts-avatar-selector.vala
index 21790696..59616c02 100644
--- a/src/contacts-avatar-selector.vala
+++ b/src/contacts-avatar-selector.vala
@@ -21,18 +21,24 @@ const int MAIN_SIZE = 128;
 const int ICONS_SIZE = 64;
 
 private class Contacts.Thumbnail : Gtk.FlowBoxChild {
+
   public Gdk.Pixbuf? source_pixbuf { get; construct set; }
+
   private Thumbnail (Gdk.Pixbuf? source_pixbuf = null) {
-    Object (visible: true, halign : Gtk.Align.CENTER, source_pixbuf: source_pixbuf);
-    this.get_style_context ().add_class ("circular");
+    Object (visible: true,
+            halign: Gtk.Align.CENTER,
+            source_pixbuf: source_pixbuf);
+
+    this.add_css_class ("circular");
+
     var avatar = new Avatar (ICONS_SIZE);
     avatar.set_pixbuf (source_pixbuf);
-    add (avatar);
+    this.set_child (avatar);
   }
 
   public Thumbnail.for_persona (Persona persona) {
     Gdk.Pixbuf? pixbuf = null;
-    var details = persona as AvatarDetails;
+    unowned var details = persona as AvatarDetails;
     if (details != null && details.avatar != null) {
       try {
         var stream = details.avatar.load (MAIN_SIZE, null);
@@ -58,72 +64,43 @@ private class Contacts.Thumbnail : Gtk.FlowBoxChild {
 /**
  * The AvatarSelector can be used to choose the avatar for a contact.
  * This can be done by either choosing a stock thumbnail, an image file
- * provided by the user, or -if cheese is enabled- by using a webcam.
+ * provided by the user, or by using a webcam.
  *
  * After a user has initially chosen an avatar, we provide a cropping tool.
  */
 [GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-avatar-selector.ui")]
-public class Contacts.AvatarSelector : Gtk.Window {
+public class Contacts.AvatarSelector : Gtk.Dialog {
+
   const string AVATAR_BUTTON_CSS_NAME = "avatar-button";
 
-  // This will provide the default thumbnails
-  private Gnome.DesktopThumbnailFactory thumbnail_factory;
-  private Individual individual;
+  private unowned Individual individual;
 
   [GtkChild]
   private unowned Gtk.FlowBox thumbnail_grid;
 
-#if HAVE_CHEESE
   [GtkChild]
-  private unowned Gtk.Button cheese_button;
-  private int num_cameras;
-  private Cheese.CameraDeviceMonitor camera_monitor;
-#endif
+  private unowned Gtk.Button camera_button;
+
+  private Xdp.Portal? portal = null;
 
   public AvatarSelector (Individual? individual, Gtk.Window? window = null) {
-    Object (transient_for: window);
-    this.thumbnail_factory = new Gnome.DesktopThumbnailFactory (Gnome.ThumbnailSize.NORMAL);
+    Object (transient_for: window, use_header_bar: 1);
     this.individual = individual;
 
-    unowned Gtk.BindingSet binding_set = Gtk.BindingSet.by_class (get_class ());
-    Gtk.BindingEntry.add_signal (binding_set,
-                                 Gdk.Key.Escape,
-                                 0,
-                                 "close",
-                                 0);
-
     update_thumbnail_grid ();
 
-#if HAVE_CHEESE
-    this.cheese_button.visible = true;
-    this.cheese_button.sensitive = false;
-
-    // Look for camera devices.
-    this.camera_monitor = new Cheese.CameraDeviceMonitor ();
-    this.camera_monitor.added.connect ( () => {
-        this.num_cameras++;
-        this.cheese_button.sensitive = (this.num_cameras > 0);
-      });
-    this.camera_monitor.removed.connect ( () => {
-        this.num_cameras--;
-        this.cheese_button.sensitive = (this.num_cameras > 0);
-      });
-    // Do this in a separate thread, or it blocks the whole UI
-    new Thread<void*> ("camera-loader", () => {
-        this.camera_monitor.coldplug ();
-        return null;
-      });
-#endif
+    this.setup_camera_portal.begin ();
   }
 
-  [Signal (action = true)]
-  public new virtual signal void close () {
-    base.close ();
-  }
+  private async void setup_camera_portal () {
+    this.portal = new Xdp.Portal ();
 
-  [GtkCallback]
-  public bool on_delete_event () {
-    return hide_on_delete ();
+    if (portal.is_camera_present ()) {
+      this.camera_button.sensitive = true;
+    } else {
+      this.camera_button.tooltip_text = _("No Camera Detected");
+      this.camera_button.sensitive = false;
+    }
   }
 
   private Gdk.Pixbuf scale_pixbuf_for_avatar_use (Gdk.Pixbuf pixbuf) {
@@ -134,10 +111,10 @@ public class Contacts.AvatarSelector : Gtk.Window {
       return pixbuf;
 
     if (w > h) {
-      h = (int)Math.round (h * (float) MAIN_SIZE / w);
+      h = (int) Math.round (h * (float) MAIN_SIZE / w);
       w = MAIN_SIZE;
     } else {
-      w = (int)Math.round (w * (float) MAIN_SIZE / h);
+      w = (int) Math.round (w * (float) MAIN_SIZE / h);
       h = MAIN_SIZE;
     }
 
@@ -150,27 +127,28 @@ public class Contacts.AvatarSelector : Gtk.Window {
       pixbuf.save_to_buffer (out buffer, "png", null);
       var icon = new BytesIcon (new Bytes (buffer));
       // Set the new avatar
-      this.individual.change_avatar.begin(icon as LoadableIcon, (obj, res) => {
+      this.individual.change_avatar.begin (icon as LoadableIcon, (obj, res) => {
         try {
-          this.individual.change_avatar.end(res);
+          this.individual.change_avatar.end (res);
         } catch (Error e) {
           warning ("Failed to set avatar: %s", e.message);
           Utils.show_error_dialog (_("Failed to set avatar."),
-                                   get_toplevel() as Gtk.Window);
+                                   get_root () as Gtk.Window);
         }
       });
     } catch (GLib.Error e) {
       warning ("Failed to set avatar: %s", e.message);
       Utils.show_error_dialog (_("Failed to set avatar."),
-                               get_toplevel() as Gtk.Window);
+                               get_root () as Gtk.Window);
     }
   }
+
   private void update_thumbnail_grid () {
     if (this.individual != null) {
       foreach (var p in individual.personas) {
         var widget = new Thumbnail.for_persona (p);
         if (widget.source_pixbuf != null)
-          this.thumbnail_grid.add (widget);
+          this.thumbnail_grid.insert (widget, -1);
       }
     }
 
@@ -178,33 +156,26 @@ public class Contacts.AvatarSelector : Gtk.Window {
     foreach (var file_name in stock_files) {
       var widget = new Thumbnail.for_filename (file_name);
       if (widget.source_pixbuf != null)
-        this.thumbnail_grid.add (widget);
+        this.thumbnail_grid.insert (widget, -1);
     }
-    this.thumbnail_grid.show_all ();
   }
 
   [GtkCallback]
-  private void on_cheese_clicked (Gtk.Button button) {
-    var dialog = new CropCheeseDialog.for_cheese (get_toplevel() as Gtk.Window);
-    dialog.show_all ();
-    dialog.picture_selected.connect ( (pix) => {
-      selected_pixbuf (scale_pixbuf_for_avatar_use (pix));
-      this.close ();
-    });
+  private void on_camera_button_clicked (Gtk.Button button) {
+    // XXX implement
+    // var dialog = new CropDialog.for_portal (this.portal,
+    //                                         this.get_root () as Gtk.Window);
+    // dialog.show ();
   }
 
-  [GtkCallback]
-  private void on_cancel_clicked (Gtk.Button button) {
-    this.close ();
-  }
-
-  [GtkCallback]
-  private void on_done_clicked (Gtk.Button button) {
-    var selected_children = thumbnail_grid.get_selected_children ();
-    if (selected_children != null) {
-      var thumbnail = (selected_children.data as Thumbnail);
-      if (thumbnail != null)
-        selected_pixbuf (scale_pixbuf_for_avatar_use (thumbnail.source_pixbuf));
+  public override void response (int response) {
+    if (response == Gtk.ResponseType.OK) {
+      var selected_children = thumbnail_grid.get_selected_children ();
+      if (selected_children != null) {
+        unowned var thumbnail = (selected_children.data as Thumbnail);
+        if (thumbnail != null)
+          selected_pixbuf (scale_pixbuf_for_avatar_use (thumbnail.source_pixbuf));
+      }
     }
 
     this.close ();
@@ -213,88 +184,52 @@ public class Contacts.AvatarSelector : Gtk.Window {
   [GtkCallback]
   private void on_file_clicked (Gtk.Button button) {
     var chooser = new Gtk.FileChooserNative (_("Browse for more pictures"),
-                                             get_toplevel () as Gtk.Window,
+                                             this.get_root () as Gtk.Window,
                                              Gtk.FileChooserAction.OPEN,
                                              _("_Open"), _("_Cancel"));
     chooser.set_modal (true);
-    chooser.set_local_only (false);
-    var preview = new Gtk.Image ();
-    preview.set_size_request (MAIN_SIZE, -1);
-    chooser.set_preview_widget (preview);
-    chooser.set_use_preview_label (false);
-    preview.show ();
-
-    chooser.update_preview.connect (update_preview);
-
-    var folder = Environment.get_user_special_dir (UserDirectory.PICTURES);
-    if (folder != null)
-      chooser.set_current_folder (folder);
-
-    chooser.response.connect ( (response) => {
-        if (response != Gtk.ResponseType.ACCEPT) {
-          chooser.destroy ();
-          return;
-        }
-        try {
-          var file = File.new_for_uri (chooser.get_uri ());
-          var in_stream = file.read ();
-          var pixbuf = new Gdk.Pixbuf.from_stream (in_stream, null);
-          in_stream.close ();
-          if (pixbuf.get_width () > MAIN_SIZE || pixbuf.get_height () > MAIN_SIZE) {
-            var dialog = new CropCheeseDialog.for_crop (get_toplevel () as Gtk.Window,
-                                                        pixbuf);
-            dialog.picture_selected.connect ( (pix) => {
-              selected_pixbuf (scale_pixbuf_for_avatar_use (pix));
-            });
-            dialog.show_all();
-          } else {
-            selected_pixbuf (scale_pixbuf_for_avatar_use (pixbuf));
-          }
-        } catch (GLib.Error e) {
-          warning ("Failed to set avatar: %s", e.message);
-          Utils.show_error_dialog (_("Failed to set avatar."),
-                                   this.get_toplevel() as Gtk.Window);
-        }
 
-      chooser.destroy ();
-    });
-
-    chooser.run ();
-    this.close ();
-  }
-
-  private void update_preview (Gtk.FileChooser chooser) {
-    var uri = chooser.get_preview_uri ();
-    if (uri != null) {
-      Gdk.Pixbuf? pixbuf = null;
+    try {
+      unowned var pictures_folder = Environment.get_user_special_dir (UserDirectory.PICTURES);
+      if (pictures_folder != null)
+        chooser.set_current_folder (File.new_for_path (pictures_folder));
+    } catch (Error e) {
+      warning ("Couldn't set avatar selector to Pictures folder: %s", e.message);
+    }
 
-      var preview = chooser.get_preview_widget () as Gtk.Image;
+    chooser.response.connect ((response) => {
+      if (response != Gtk.ResponseType.ACCEPT) {
+        chooser.destroy ();
+        return;
+      }
 
-      var file = File.new_for_uri (uri);
       try {
-        var file_info = file.query_info (FileAttribute.STANDARD_CONTENT_TYPE,
-                         FileQueryInfoFlags.NONE, null);
-        if (file_info != null) {
-          var mime_type = file_info.get_content_type ();
-
-          if (mime_type != null)
-            pixbuf = this.thumbnail_factory.generate_thumbnail (uri, mime_type);
+        var file = chooser.get_file ();
+        var in_stream = file.read ();
+        var pixbuf = new Gdk.Pixbuf.from_stream (in_stream, null);
+        in_stream.close ();
+        if (pixbuf.get_width () > MAIN_SIZE || pixbuf.get_height () > MAIN_SIZE) {
+          var dialog = new CropDialog.for_pixbuf (pixbuf,
+                                                  get_root () as Gtk.Window);
+          dialog.response.connect ((response) => {
+              if (response == Gtk.ResponseType.ACCEPT) {
+                var cropped = dialog.create_pixbuf ();
+                selected_pixbuf (scale_pixbuf_for_avatar_use (cropped));
+              }
+              dialog.destroy ();
+          });
+          dialog.show ();
+        } else {
+          selected_pixbuf (scale_pixbuf_for_avatar_use (pixbuf));
         }
-      } catch (Error e) {
-        debug ("Couldn't generate thumbnail for file '%s': %s", uri, e.message);
+      } catch (GLib.Error e) {
+        warning ("Failed to set avatar: %s", e.message);
+        Utils.show_error_dialog (_("Failed to set avatar."),
+                                 this.get_root () as Gtk.Window);
+      } finally {
+        chooser.destroy ();
       }
-
-      if (chooser is Gtk.Dialog)
-        ((Gtk.Dialog) chooser).set_response_sensitive (Gtk.ResponseType.ACCEPT,
-                                                       (pixbuf != null));
-
-      if (pixbuf != null)
-        preview.set_from_pixbuf (pixbuf);
-      else
-        preview.set_from_icon_name ("dialog-question", Gtk.IconSize.DIALOG);
-    }
-
-    chooser.set_preview_widget_active (true);
+    });
+    chooser.show ();
   }
-
 }
diff --git a/src/contacts-avatar.vala b/src/contacts-avatar.vala
index 33653065..3f8b4bab 100644
--- a/src/contacts-avatar.vala
+++ b/src/contacts-avatar.vala
@@ -21,13 +21,17 @@ using Folks;
  * The Avatar of a Contact is responsible for showing an {@link Folks.Individual}'s
  * avatar, or a fallback if it's not available.
  */
-public class Contacts.Avatar : Gtk.Bin {
-  private Hdy.Avatar widget;
+public class Contacts.Avatar : Adw.Bin {
 
   private unowned Individual? individual = null;
 
+  private int avatar_size;
+  private bool load_avatar_started = false;
+
   public Avatar (int size, Individual? individual = null) {
     this.individual = individual;
+    this.avatar_size = size;
+
     string name = "";
     bool show_initials = false;
     if (this.individual != null) {
@@ -42,35 +46,43 @@ public class Contacts.Avatar : Gtk.Bin {
       }
     }
 
-    this.widget = new Hdy.Avatar (size, name, show_initials);
-    this.widget.set_image_load_func (size => load_avatar (size));
-    this.widget.show ();
-    add(this.widget);
+    this.child = new Adw.Avatar (size, name, show_initials);
 
-    show ();
+    // FIXME: ideally we lazy-load this only when we become visible for the
+    // first time
+    this.load_avatar.begin ();
+  }
+
+  public async void load_avatar() {
+    if (this.load_avatar_started)
+      return;
+
+    if (individual == null || individual.avatar == null)
+      return;
+
+    this.load_avatar_started = true;
+
+    try {
+      var stream = yield this.individual.avatar.load_async (this.avatar_size,
+                                                            null);
+      var pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async (stream,
+                                                                    this.avatar_size,
+                                                                    this.avatar_size,
+                                                                    true);
+      this.set_pixbuf (pixbuf);
+    } catch (Error e) {
+      warning ("Couldn't load avatar of '%s': %s", this.individual.display_name, e.message);
+    }
   }
 
   /**
    * Manually set the avatar to the given pixbuf, even if the contact has an avatar.
    */
   public void set_pixbuf (Gdk.Pixbuf? a_pixbuf) {
-    this.widget.set_image_load_func (size => load_avatar (size, a_pixbuf));
-  }
-
-  private Gdk.Pixbuf? load_avatar (int size, Gdk.Pixbuf? pixbuf = null) {
-    if (pixbuf != null) {
-      return pixbuf.scale_simple (size, size, Gdk.InterpType.HYPER);
-    } else {
-      if (this.individual != null && this.individual.avatar != null) {
-        try {
-          var stream = this.individual.avatar.load (size, null);
-          return new Gdk.Pixbuf.from_stream_at_scale (stream, size, size, true);
-        } catch (Error e) {
-          debug ("Couldn't load avatar of contact %s. Reason: %s", this.individual.display_name, e.message);
-        }
-      }
-    }
-    return null;
+    if (a_pixbuf != null)
+      ((Adw.Avatar) this.child).set_custom_image (Gdk.Texture.for_pixbuf (a_pixbuf));
+    else
+      ((Adw.Avatar) this.child).set_icon_name ("avatar-default-symbolic");
   }
 
   /* Find a nice name to generate the label and color for the fallback avatar
@@ -78,29 +90,33 @@ public class Contacts.Avatar : Gtk.Bin {
    * as a display name which we don't want to have as a label
    */
   private string find_display_name () {
-    string name = "";
-    Persona primary_persona = null;
+    unowned Persona primary_persona = null;
     foreach (var p in this.individual.personas) {
       if (p.store.is_primary_store) {
         primary_persona = p;
         break;
       }
     }
-    name = look_up_alias_for_display_name (primary_persona);
-    if (name == "") {
-      foreach (var p in this.individual.personas) {
-        name = look_up_alias_for_display_name (p);
-      }
+
+    unowned string alias = look_up_alias_for_display_name (primary_persona);
+    if (alias != "")
+      return alias;
+
+    foreach (var p in this.individual.personas) {
+      alias = look_up_alias_for_display_name (p);
+      if (alias != "")
+        return alias;
     }
-    if (name == "") {
-      foreach (var p in this.individual.personas) {
-        name = look_up_name_details_for_display_name (p);
-      }
+
+    foreach (var p in this.individual.personas) {
+      string name = look_up_name_details_for_display_name (p);
+      if (name != "")
+        return name;
     }
-    return name;
+    return "";
   }
 
-  private string look_up_alias_for_display_name (Persona? p) {
+  private unowned string look_up_alias_for_display_name (Persona? p) {
     unowned var a = p as AliasDetails;
     if (a != null && a.alias != null)
       return a.alias;
diff --git a/src/contacts-contact-editor.vala b/src/contacts-contact-editor.vala
index 85c34f74..dd2f764b 100644
--- a/src/contacts-contact-editor.vala
+++ b/src/contacts-contact-editor.vala
@@ -1,6 +1,7 @@
 /*
  * Copyright (C) 2011 Alexander Larsson <alexl redhat com>
  * Copyright (C) 2019 Purism SPC
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -22,24 +23,29 @@ using Folks;
  * A widget that allows the user to edit a given {@link Contact}.
  */
 public class Contacts.ContactEditor : Gtk.Box {
+
   private Individual individual;
   private Gtk.Entry name_entry;
-  private AvatarSelector avatar_selector = null;
   private Avatar avatar;
 
+  construct {
+    this.orientation = Gtk.Orientation.VERTICAL;
+    this.spacing = 12;
+
+    this.add_css_class ("contacts-contact-editor");
+  }
+
   public ContactEditor (Individual individual, IndividualAggregator aggregator) {
-    Object (orientation: Gtk.Orientation.VERTICAL, spacing: 24);
     this.individual = individual;
 
     Gtk.Box header = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6);
-    header.add (create_avatar_button ());
-    header.add (create_name_entry ());
-    add (header);
+    header.append (create_avatar_button ());
+    header.append (create_name_entry ());
+    append (header);
 
     foreach (var p in individual.personas) {
-      add (new EditorPersona (p, aggregator));
+      append (new EditorPersona (p, aggregator));
     }
-    show_all ();
   }
 
   // Creates the contact's current avatar in a big button on top of the Editor
@@ -47,8 +53,8 @@ public class Contacts.ContactEditor : Gtk.Box {
     this.avatar = new Avatar (PROFILE_SIZE, this.individual);
 
     var button = new Gtk.Button ();
-    button.get_accessible ().set_name (_("Change avatar"));
-    button.image = this.avatar;
+    button.tooltip_text = _("Change avatar");
+    button.set_child (this.avatar);
     button.clicked.connect (on_avatar_button_clicked);
 
     return button;
@@ -56,9 +62,11 @@ public class Contacts.ContactEditor : Gtk.Box {
 
   // Show the avatar popover when the avatar is clicked
   private void on_avatar_button_clicked (Gtk.Button avatar_button) {
-    if (this.avatar_selector == null)
-      this.avatar_selector = new AvatarSelector (this.individual, (Gtk.Window) this.get_toplevel());
-    this.avatar_selector.show();
+    var avatar_selector = new AvatarSelector (this.individual, get_root () as Gtk.Window);
+    avatar_selector.response.connect (() => {
+      avatar_selector.destroy ();
+    });
+    avatar_selector.show ();
   }
 
   // Creates the big name entry on the top
diff --git a/src/contacts-contact-list.vala b/src/contacts-contact-list.vala
index 266545d8..247e7aaf 100644
--- a/src/contacts-contact-list.vala
+++ b/src/contacts-contact-list.vala
@@ -22,58 +22,7 @@ using Folks;
  * the left. It is contained by the {@link ListPane}, which also provides other
  * functionality, such as an action bar.
  */
-public class Contacts.ContactList : Gtk.ListBox {
-  private class ContactDataRow : Gtk.ListBoxRow {
-    private const int LIST_AVATAR_SIZE = 48;
-
-    public unowned Individual individual;
-    private Gtk.Label label;
-    private Avatar avatar;
-    public Gtk.CheckButton selector_button;
-
-    public ContactDataRow(Individual i) {
-      this.individual = i;
-      this.individual.notify.connect (on_contact_changed);
-
-      get_style_context (). add_class ("contact-data-row");
-
-      Gtk.Grid grid = new Gtk.Grid ();
-      grid.margin = 3;
-      grid.margin_start = 9;
-      grid.set_column_spacing (10);
-      this.avatar = new Avatar (LIST_AVATAR_SIZE, this.individual);
-
-      this.label = new Gtk.Label (individual.display_name);
-      this.label.ellipsize = Pango.EllipsizeMode.END;
-      this.label.valign = Gtk.Align.CENTER;
-      this.label.halign = Gtk.Align.START;
-      // Make sure it doesn't "twitch" when the checkbox becomes visible
-      this.label.xalign = 0;
-
-      this.selector_button = new Gtk.CheckButton ();
-      this.selector_button.visible = false;
-      this.selector_button.valign = Gtk.Align.CENTER;
-      this.selector_button.halign = Gtk.Align.END;
-      this.selector_button.hexpand = true;
-      // Make sure it doesn't overlap with the scrollbar
-      this.selector_button.margin_end = 12;
-
-      grid.attach (this.avatar, 0, 0);
-      grid.attach (this.label, 1, 0);
-      grid.attach (this.selector_button, 2, 0);
-      this.add (grid);
-      this.show_all ();
-    }
-
-    private void on_contact_changed (Object obj, ParamSpec pspec) {
-      //TODO: Update also the Avatar
-      this.label.set_text (this.individual.display_name);
-      changed ();
-    }
-  }
-
-  public signal void selection_changed (Individual? individual);
-  public signal void contacts_marked (int contacts_marked);
+public class Contacts.ContactList : Adw.Bin {
 
   int nr_contacts_marked = 0;
 
@@ -83,51 +32,87 @@ public class Contacts.ContactList : Gtk.ListBox {
 
   private bool sort_on_surname = false; // keep in sync with the setting
 
-  private Gtk.GestureLongPress long_press;
   private bool got_long_press = false;
 
   public UiState state { get; set; }
 
-  public ContactList (Settings settings, Store store, Query query) {
-    this.selection_mode = Gtk.SelectionMode.BROWSE;
+  private unowned Gtk.ListBox listbox;
+
+  // The vertical adjustment of the scrolled window
+  private unowned Gtk.Adjustment vadjustment;
+
+  public signal void selection_changed (Individual? individual);
+  public signal void contacts_marked (int contacts_marked);
+
+  construct {
+    // First construct a ScrolledWindow with a Viewport
+    var sw = new Gtk.ScrolledWindow ();
+    sw.hscrollbar_policy = Gtk.PolicyType.NEVER;
+    sw.add_css_class ("contact-list-scrolled-window");
+    this.vadjustment = sw.vadjustment;
+    sw.vadjustment.value_changed.connect ((vadj) => { this.load_visible_avatars (); });
+    this.child = sw;
+
+    var viewport = new Gtk.Viewport (sw.hadjustment, sw.vadjustment);
+    viewport.scroll_to_focus = true;
+    sw.set_child (viewport);
+
+    // Then create the listbox
+    var list_box = new Gtk.ListBox ();
+    this.listbox = list_box;
+    viewport.set_child (list_box);
+
+    this.listbox.selection_mode = Gtk.SelectionMode.BROWSE;
+    this.listbox.set_sort_func (compare_rows);
+    this.listbox.set_filter_func (filter_row);
+    this.listbox.set_header_func (update_header);
+    this.listbox.add_css_class ("navigation-sidebar");
+
+    this.add_css_class ("contacts-contact-list");
+
+    // Row selection/activation
+    this.listbox.row_activated.connect (on_row_activated);
+    this.listbox.row_selected.connect (on_row_selected);
+
+    // Connect events right-click and long-press
+    var secondary_click_gesture = new Gtk.GestureClick ();
+    secondary_click_gesture.button = Gdk.BUTTON_SECONDARY;
+    secondary_click_gesture.pressed.connect (on_right_click);
+    this.listbox.add_controller (secondary_click_gesture);
+
+    var long_press_gesture = new Gtk.GestureLongPress ();
+    long_press_gesture.pressed.connect (on_long_press);
+    this.listbox.add_controller (long_press_gesture);
+  }
+
+  public ContactList (Settings settings,
+                      Store    store,
+                      Query    query) {
     this.store = store;
     this.filter_query = query;
-    this.filter_query.notify.connect (() => { invalidate_filter (); });
-    this.visible = true;
+    this.filter_query.notify.connect (() => { this.listbox.invalidate_filter ();
+                                      });
 
     this.notify["state"].connect (on_ui_state_changed);
 
-    // Connect long press gesture
-    this.long_press = new Gtk.GestureLongPress (this);
-    this.long_press.pressed.connect ((g, x, y) => {
-      this.got_long_press = true;
-      var row = (ContactDataRow) get_row_at_y ((int) Math.round (y));
-      if (row != null) {
-        row.selector_button.active = this.state != UiState.SELECTING || !row.selector_button.active;
-      }
-    });
-
     this.sort_on_surname = settings.sort_on_surname;
-    settings.changed["sort-on-surname"].connect(() => {
-        this.sort_on_surname = settings.sort_on_surname;
-        invalidate_sort();
-      });
+    settings.changed["sort-on-surname"].connect (() => {
+      this.sort_on_surname = settings.sort_on_surname;
+      this.listbox.invalidate_sort ();
+    });
 
     this.store.added.connect (contact_added_cb);
     this.store.removed.connect (contact_removed_cb);
     foreach (var i in this.store.get_contacts ())
       contact_added_cb (this.store, i);
-
-    get_style_context ().add_class ("contacts-contact-list");
-
-    set_sort_func (compare_rows);
-    set_filter_func (filter_row);
-    set_header_func (update_header);
   }
 
   private void on_ui_state_changed (Object obj, ParamSpec pspec) {
-    foreach (var widget in get_children ()) {
-      var row = widget as ContactDataRow;
+    for (int i = 0; true; i++) {
+      unowned var row = (ContactDataRow) this.listbox.get_row_at_index (i);
+      if (row == null)
+        break;
+
       row.selector_button.visible = (this.state == UiState.SELECTING);
 
       if (this.state != UiState.SELECTING)
@@ -136,9 +121,9 @@ public class Contacts.ContactList : Gtk.ListBox {
 
     // Disalbe highlighted (blue) selection since we use the checkbox to show selection
     if (this.state == UiState.SELECTING) {
-      this.selection_mode = Gtk.SelectionMode.NONE;
+      this.listbox.selection_mode = Gtk.SelectionMode.NONE;
     } else {
-      this.selection_mode = Gtk.SelectionMode.BROWSE;
+      this.listbox.selection_mode = Gtk.SelectionMode.BROWSE;
       this.nr_contacts_marked = 0;
     }
   }
@@ -188,14 +173,12 @@ public class Contacts.ContactList : Gtk.ListBox {
   private Gtk.Label create_header_label (string text) {
     var label = new Gtk.Label (text);
     label.halign = Gtk.Align.START;
-    label.margin = 3;
     label.margin_start = 6;
+    label.margin_end = 3;
     label.margin_top = 6;
-    var attrs = new Pango.AttrList ();
-    attrs.insert (Pango.attr_weight_new (Pango.Weight.BOLD));
-    attrs.insert (Pango.attr_scale_new ((Pango.Scale.SMALL + Pango.Scale.MEDIUM) / 2.0));
-    attrs.insert (Pango.attr_foreground_alpha_new (30000));
-    label.attributes = attrs;
+    label.margin_bottom = 3;
+    label.add_css_class ("heading");
+    label.add_css_class ("dim-label");
     return label;
   }
 
@@ -203,10 +186,10 @@ public class Contacts.ContactList : Gtk.ListBox {
     // Don't create a row for ignorable contacts are the individual already has a row
     if (!Contacts.Utils.is_ignorable (i) && find_row_for_contact(i) == null) {
       var row =  new ContactDataRow (i);
-      row.selector_button.toggled.connect ( () => { on_row_checkbox_toggled (row); });
+      row.selector_button.toggled.connect (() => { on_row_checkbox_toggled (row); });
       row.selector_button.visible = (this.state == UiState.SELECTING);
 
-      add (row);
+      this.listbox.append (row);
     } else {
       debug ("Contact %s was ignored", i.id);
     }
@@ -228,9 +211,9 @@ public class Contacts.ContactList : Gtk.ListBox {
       row.destroy ();
   }
 
-  public override void row_activated (Gtk.ListBoxRow row) {
+  private void on_row_activated (Gtk.ListBox listbox, Gtk.ListBoxRow row) {
     if (!this.got_long_press) {
-      var data = row as ContactDataRow;
+      unowned var data = row as ContactDataRow;
       if (data != null && this.state == UiState.SELECTING)
         data.selector_button.active = !data.selector_button.active;
     } else {
@@ -238,10 +221,10 @@ public class Contacts.ContactList : Gtk.ListBox {
     }
   }
 
-  public override void row_selected (Gtk.ListBoxRow? row) {
+  private void on_row_selected (Gtk.ListBox listbox, Gtk.ListBoxRow? row) {
     if (this.state != UiState.SELECTING) {
-      var data = row as ContactDataRow;
-      var individual = data != null ? data.individual : null;
+      unowned var data = (ContactDataRow?) row;
+      unowned var individual = data != null? data.individual : null;
       selection_changed (individual);
 #if HAVE_TELEPATHY
       if (individual != null)
@@ -251,29 +234,32 @@ public class Contacts.ContactList : Gtk.ListBox {
   }
 
   private bool filter_row (Gtk.ListBoxRow row) {
-    var individual = ((ContactDataRow) row).individual;
+    unowned var individual = ((ContactDataRow) row).individual;
     return this.filter_query.is_match (individual) > 0;
   }
 
   public void select_contact (Individual? individual) {
     if (individual == null) {
       /* deselect */
-      select_row (null);
+      this.listbox.select_row (null);
       return;
     }
 
-    ContactDataRow? row = find_row_for_contact (individual);
-    select_row (row);
+    unowned var row = find_row_for_contact (individual);
+    this.listbox.select_row (row);
     scroll_to_contact (row);
   }
 
+  private void load_visible_avatars () {
+    // FIXME: use the vadjustment to load only the avatars of the visible rows
+  }
+
   public void scroll_to_contact (Gtk.ListBoxRow? row = null) {
     unowned ContactDataRow? selected_row = null;
-
     if (row == null)
-      selected_row = get_selected_row () as ContactDataRow;
+      selected_row = (ContactDataRow?) this.listbox.get_selected_row ();
     else
-      selected_row = row as ContactDataRow;
+      selected_row = (ContactDataRow) row;
 
     GLib.Timeout.add (100, () => {
       if (selected_row != null)
@@ -282,16 +268,18 @@ public class Contacts.ContactList : Gtk.ListBox {
     });
   }
 
-  public void hide_contact (Individual? individual) {
+  public void set_contact_visible (Individual? individual, bool visible) {
     if (individual != null) {
-      find_row_for_contact (individual).hide ();
+      find_row_for_contact (individual).visible = visible;
     }
   }
 
-
   private unowned ContactDataRow? find_row_for_contact (Individual individual) {
-    foreach (weak Gtk.Widget widget in get_children ()) {
-      unowned var row = ((ContactDataRow) widget);
+    for (int i = 0; true; i++) {
+      unowned var row = (ContactDataRow) this.listbox.get_row_at_index (i);
+      if (row == null)
+        break;
+
       if (row.individual == individual)
         return row;
     }
@@ -301,18 +289,27 @@ public class Contacts.ContactList : Gtk.ListBox {
 
   public Gee.LinkedList<Individual> get_marked_contacts () {
     var cs = new Gee.LinkedList<Individual> ();
-    foreach (weak Gtk.Widget widget in get_children ()) {
-      unowned var row = widget as ContactDataRow;
+
+    for (int i = 0; true; i++) {
+      unowned var row = (ContactDataRow) this.listbox.get_row_at_index (i);
+      if (row == null)
+        break;
+
       if (row.selector_button.active)
         cs.add (row.individual);
     }
+
     return cs;
   }
 
   public Gee.LinkedList<Individual> get_marked_contacts_and_hide () {
     var cs = new Gee.LinkedList<Individual> ();
-    foreach (weak Gtk.Widget widget in get_children ()) {
-      unowned var row = widget as ContactDataRow;
+
+    for (int i = 0; true; i++) {
+      unowned var row = (ContactDataRow) this.listbox.get_row_at_index (i);
+      if (row == null)
+        break;
+
       if (row.selector_button.active) {
         row.visible = false;
         cs.add (row.individual);
@@ -321,17 +318,71 @@ public class Contacts.ContactList : Gtk.ListBox {
     return cs;
   }
 
+  private void on_right_click (Gtk.GestureClick gesture, int n_press, double x, double y) {
+    unowned var row = (ContactDataRow) this.listbox.get_row_at_y ((int) Math.round (y));
+    if (row != null) {
+      row.selector_button.active = this.state != UiState.SELECTING || !row.selector_button.active;
+    }
+  }
 
-  public override bool button_press_event (Gdk.EventButton event) {
-    base.button_press_event (event);
+  private void on_long_press (Gtk.GestureLongPress gesture, double x, double y) {
+    this.got_long_press = true;
+    unowned var row = (ContactDataRow) this.listbox.get_row_at_y ((int) Math.round (y));
+    if (row != null) {
+      row.selector_button.active = this.state != UiState.SELECTING || !row.selector_button.active;
+    }
+  }
 
-    if (event.button == Gdk.BUTTON_SECONDARY) {
-      unowned var row = (ContactDataRow) get_row_at_y ((int) Math.round (event.y));
-      if (row != null) {
-        row.selector_button.active = this.state != UiState.SELECTING || !row.selector_button.active;
-      }
+  // A class for the ListBoxRows
+  private class ContactDataRow : Gtk.ListBoxRow {
+    private const int LIST_AVATAR_SIZE = 48;
+
+    public unowned Individual individual;
+    private unowned Gtk.Label label;
+    private unowned Avatar avatar;
+    public unowned Gtk.CheckButton selector_button;
+
+    public ContactDataRow (Individual i) {
+      this.individual = i;
+      this.individual.notify.connect (on_contact_changed);
+
+      add_css_class ("contact-data-row");
+
+      var box = new Gtk.Box (HORIZONTAL, 12);
+      box.margin_top = 6;
+      box.margin_bottom = 6;
+
+      var avatar = new Avatar (LIST_AVATAR_SIZE, this.individual);
+      box.append (avatar);
+      this.avatar = avatar;
+
+      var label = new Gtk.Label (individual.display_name);
+      label.ellipsize = Pango.EllipsizeMode.END;
+      label.valign = Gtk.Align.CENTER;
+      label.halign = Gtk.Align.START;
+      // Make sure it doesn't "twitch" when the checkbox becomes visible
+      label.xalign = 0;
+      box.append (label);
+      this.label = label;
+
+      var selector_button = new Gtk.CheckButton ();
+      selector_button.visible = false;
+      selector_button.valign = Gtk.Align.CENTER;
+      selector_button.halign = Gtk.Align.END;
+      selector_button.hexpand = true;
+      selector_button.add_css_class ("selection-mode");
+      // Make sure it doesn't overlap with the scrollbar
+      selector_button.margin_end = 12;
+      box.append (selector_button);
+      this.selector_button = selector_button;
+
+      this.set_child (box);
     }
 
-    return false;
+    private void on_contact_changed (Object obj, ParamSpec pspec) {
+      //TODO: Update also the Avatar
+      this.label.set_text (this.individual.display_name);
+      changed ();
+    }
   }
 }
diff --git a/src/contacts-contact-pane.vala b/src/contacts-contact-pane.vala
index bb52992e..97262161 100644
--- a/src/contacts-contact-pane.vala
+++ b/src/contacts-contact-pane.vala
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2011 Alexander Larsson <alexl redhat com>
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -25,7 +26,7 @@ const int PROFILE_SIZE = 128;
  * and a ContactEditor to edit contact information.
  */
 [GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-contact-pane.ui")]
-public class Contacts.ContactPane : Gtk.Bin {
+public class Contacts.ContactPane : Adw.Bin {
 
   private MainWindow main_window;
 
@@ -37,21 +38,12 @@ public class Contacts.ContactPane : Gtk.Bin {
   private unowned Gtk.Stack stack;
 
   [GtkChild]
-  private unowned Hdy.StatusPage none_selected_page;
+  private unowned Adw.Clamp contact_sheet_clamp;
+  private unowned ContactSheet? sheet = null;
 
   [GtkChild]
-  private unowned Gtk.ScrolledWindow contact_sheet_view;
-
-  [GtkChild]
-  private unowned Gtk.Container contact_sheet_page;
-  private ContactSheet? sheet = null;
-
-  [GtkChild]
-  private unowned Gtk.ScrolledWindow contact_editor_view;
-
-  [GtkChild]
-  private unowned Gtk.Box contact_editor_page;
-  private ContactEditor? editor = null;
+  private unowned Gtk.Box contact_editor_box;
+  private unowned ContactEditor? editor = null;
 
   public bool on_edit_mode = false;
   private LinkSuggestionGrid? suggestion_grid = null;
@@ -70,28 +62,29 @@ public class Contacts.ContactPane : Gtk.Bin {
   }
 
   public void add_suggestion (Individual i) {
-    var parent_overlay = this.get_parent () as Gtk.Overlay;
+    unowned var parent_overlay = this.get_parent () as Gtk.Overlay;
 
     remove_suggestion_grid ();
     this.suggestion_grid = new LinkSuggestionGrid (i);
+    this.suggestion_grid.valign = Gtk.Align.END;
     parent_overlay.add_overlay (this.suggestion_grid);
 
-    this.suggestion_grid.suggestion_accepted.connect ( () => {
-        var linked_contact = this.individual.display_name;
-        var operation = new LinkOperation (this.store);
-        var to_link = new Gee.LinkedList<Individual> ();
-        to_link.add (this.individual);
-        to_link.add (i);
-        operation.execute.begin (to_link);
-        this.contacts_linked (null, linked_contact, operation);
-        remove_suggestion_grid ();
-      });
-
-    this.suggestion_grid.suggestion_rejected.connect ( () => {
-        /* TODO: Add undo */
-        store.add_no_suggest_link (this.individual, i);
-        remove_suggestion_grid ();
-      });
+    this.suggestion_grid.suggestion_accepted.connect (() => {
+      var linked_contact = this.individual.display_name;
+      var to_link = new Gee.LinkedList<Individual> ();
+      to_link.add (this.individual);
+      to_link.add (i);
+      var operation = new LinkOperation (this.store, to_link);
+      operation.execute.begin ();
+      this.contacts_linked (null, linked_contact, operation);
+      remove_suggestion_grid ();
+    });
+
+    this.suggestion_grid.suggestion_rejected.connect (() => {
+      /* TODO: Add undo */
+      store.add_no_suggest_link (this.individual, i);
+      remove_suggestion_grid ();
+    });
   }
 
   public void show_contact (Individual? individual) {
@@ -104,17 +97,19 @@ public class Contacts.ContactPane : Gtk.Bin {
       show_contact_sheet ();
     } else {
       remove_contact_sheet ();
-      this.stack.set_visible_child (this.none_selected_page);
+      this.stack.set_visible_child_name ("none-selected-page");
     }
   }
 
   private void show_contact_sheet () {
-    assert (this.individual != null);
+    return_if_fail (this.individual != null);
 
-    remove_contact_sheet();
-    this.sheet = new ContactSheet (this.individual, this.store);
-    this.contact_sheet_page.add (this.sheet);
-    this.stack.set_visible_child (this.contact_sheet_view);
+    remove_contact_sheet ();
+    var contacts_sheet = new ContactSheet (this.individual, this.store);
+    contacts_sheet.hexpand = true;
+    this.sheet = contacts_sheet;
+    this.contact_sheet_clamp.set_child (this.sheet);
+    this.stack.set_visible_child_name ("contact-sheet-page");
 
     var matches = this.store.aggregator.get_potential_matches (this.individual, MatchResult.HIGH);
     foreach (var i in matches.keys) {
@@ -132,24 +127,24 @@ public class Contacts.ContactPane : Gtk.Bin {
     // Remove the suggestion grid that goes along with it.
     remove_suggestion_grid ();
 
-    this.contact_sheet_page.remove (this.sheet);
-    this.sheet.destroy();
+    this.contact_sheet_clamp.set_child (null);
     this.sheet = null;
   }
 
   private void create_contact_editor () {
     remove_contact_editor ();
 
-    this.editor = new ContactEditor (this.individual, store.aggregator);
+    var contact_editor = new ContactEditor (this.individual, store.aggregator);
+    this.editor = contact_editor;
 
-    this.contact_editor_page.add (this.editor);
+    this.contact_editor_box.append (this.editor);
   }
 
   private void remove_contact_editor () {
     if (this.editor == null)
       return;
 
-    this.contact_editor_page.remove (this.editor);
+    this.contact_editor_box.remove (this.editor);
     this.editor = null;
   }
 
@@ -160,7 +155,7 @@ public class Contacts.ContactPane : Gtk.Bin {
     this.on_edit_mode = true;
 
     create_contact_editor ();
-    this.stack.set_visible_child (this.contact_editor_view);
+    this.stack.set_visible_child_name ("contact-editor-page");
   }
 
   public void stop_editing (bool cancel = false) {
@@ -175,9 +170,9 @@ public class Contacts.ContactPane : Gtk.Bin {
       if (fake_individual != null && fake_individual.real_individual != null) {
         // Reset individual on to the real one
         this.individual = fake_individual.real_individual;
-        this.stack.set_visible_child (this.contact_sheet_view);
+        this.stack.set_visible_child_name ("contact-sheet-page");
       } else {
-        this.stack.set_visible_child (this.none_selected_page);
+        this.stack.set_visible_child_name ("none-selected-page");
       }
       return;
     }
@@ -220,10 +215,10 @@ public class Contacts.ContactPane : Gtk.Bin {
       writeable_properties = {};
     }
 
-    var fake_persona = new FakePersona (FakePersonaStore.the_store(), writeable_properties, details);
+    var fake_persona = new FakePersona (FakePersonaStore.the_store (), writeable_properties, details);
     var fake_personas = new Gee.HashSet<FakePersona> ();
     fake_personas.add (fake_persona);
-    this.individual = new FakeIndividual(fake_personas);
+    this.individual = new FakeIndividual (fake_personas);
 
     start_editing ();
   }
@@ -262,20 +257,21 @@ public class Contacts.ContactPane : Gtk.Bin {
 
   private void show_message_dialog (string message) {
     var dialog =
-        new Gtk.MessageDialog (this.main_window,
-                               Gtk.DialogFlags.DESTROY_WITH_PARENT | Gtk.DialogFlags.MODAL,
-                               Gtk.MessageType.ERROR,
-                               Gtk.ButtonsType.OK,
-                               "%s", message);
-    dialog.run ();
-    dialog.destroy ();
+      new Gtk.MessageDialog (this.main_window,
+                             Gtk.DialogFlags.DESTROY_WITH_PARENT | Gtk.DialogFlags.MODAL,
+                             Gtk.MessageType.ERROR,
+                             Gtk.ButtonsType.OK,
+                             "%s", message);
+    dialog.response.connect ((_) => dialog.close ());
+    dialog.show ();
   }
 
   private void remove_suggestion_grid () {
     if (this.suggestion_grid == null)
       return;
 
-    this.suggestion_grid.destroy ();
+    unowned var parent_overlay = this.get_parent () as Gtk.Overlay;
+    parent_overlay.remove_overlay (suggestion_grid);
     this.suggestion_grid = null;
   }
 }
diff --git a/src/contacts-contact-sheet.vala b/src/contacts-contact-sheet.vala
index ceb41a54..43aae398 100644
--- a/src/contacts-contact-sheet.vala
+++ b/src/contacts-contact-sheet.vala
@@ -17,16 +17,42 @@
 
 using Folks;
 
+public class Contacts.ContactSheetRow : Adw.ActionRow {
+
+  public ContactSheetRow (string property_name, string title, string? subtitle = null) {
+    unowned var icon_name = Utils.get_icon_name_for_property (property_name);
+    if (icon_name != null) {
+      var icon = new Gtk.Image.from_icon_name (icon_name);
+      icon.add_css_class ("contacts-property-icon");
+      icon.tooltip_text = Utils.get_display_name_for_property (property_name);
+      this.add_prefix (icon);
+    }
+
+    this.title = Markup.escape_text (title);
+
+    if (subtitle != null)
+      this.subtitle = subtitle;
+  }
+
+  public Gtk.Button add_button (string icon) {
+    var button = new Gtk.Button.from_icon_name (icon);
+    button.valign = Gtk.Align.CENTER;
+    button.add_css_class ("flat");
+    this.add_suffix (button);
+    return button;
+  }
+}
+
 /**
  * The contact sheet displays the actual information of a contact.
  *
  * (Note: to edit a contact, use the {@link ContactEditor} instead.
  */
 public class Contacts.ContactSheet : Gtk.Grid {
+
   private int last_row = 0;
-  private Individual individual;
+  private unowned Individual individual;
   private unowned Store store;
-  public bool narrow { get; set; default = true; }
 
   private const string[] SORTED_PROPERTIES = {
     "email-addresses",
@@ -39,8 +65,11 @@ public class Contacts.ContactSheet : Gtk.Grid {
     "notes"
   };
 
+  construct {
+    this.add_css_class ("contacts-sheet");
+  }
+
   public ContactSheet (Individual individual, Store store) {
-    Object (row_spacing: 12, column_spacing: 12);
     this.individual = individual;
     this.store = store;
 
@@ -56,140 +85,112 @@ public class Contacts.ContactSheet : Gtk.Grid {
     var attrList = new Pango.AttrList ();
     attrList.insert (Pango.attr_weight_new (Pango.Weight.BOLD));
     store_name.set_attributes (attrList);
-    store_name.set_halign (Gtk.Align.START);
-    store_name.set_ellipsize (Pango.EllipsizeMode.MIDDLE);
+    store_name.halign = Gtk.Align.START;
+    store_name.ellipsize = Pango.EllipsizeMode.MIDDLE;
 
     return store_name;
   }
 
-  private Gtk.Button create_button (string icon) {
-    var button = new Gtk.Button.from_icon_name (icon, Gtk.IconSize.BUTTON);
-    button.set_halign (Gtk.Align.END);
-    button.get_style_context ().add_class ("flatten");
+  // Helper function that attaches a row to our grid
+  private void attach_row (Gtk.ListBoxRow row) {
+    var list_box = new Gtk.ListBox ();
+    list_box.selection_mode = Gtk.SelectionMode.NONE;
+    list_box.add_css_class ("boxed-list");
+    list_box.add_css_class ("contacts-sheet-property");
+    list_box.append (row);
 
-    return button;
-  }
-
-  void add_row_with_label (string label_value,
-                           string value,
-                           Gtk.Widget? btn1 = null,
-                           Gtk.Widget? btn2 =null) {
-    if (value == "" || value == null)
-      return;
-    var type_label = new Gtk.Label (label_value);
-    type_label.xalign = 1.0f;
-    type_label.set_halign (Gtk.Align.END);
-    type_label.set_valign (Gtk.Align.CENTER);
-    type_label.get_style_context ().add_class ("dim-label");
-    this.attach (type_label, 0, this.last_row, 1, 1);
-
-    var value_label = new Gtk.Label (value);
-    value_label.set_line_wrap (true);
-    value_label.xalign = 0.0f;
-    value_label.set_halign (Gtk.Align.START);
-    value_label.set_ellipsize (Pango.EllipsizeMode.END);
-    value_label.wrap_mode = Pango.WrapMode.CHAR;
-    value_label.set_selectable (true);
-    value_label.set_can_focus (false);
-
-    if (btn1 != null || btn2 !=null) {
-      var value_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 12);
-      value_box.pack_start (value_label, false, false, 0);
-
-      if (btn1 != null)
-        value_box.pack_end (btn1, false, false, 0);
-      if (btn2 != null)
-        value_box.pack_end (btn2, false, false, 0);
-      this.attach (value_box, 1, this.last_row, 1, 1);
-    } else {
-      this.attach (value_label, 1, this.last_row, 1, 1);
-    }
+    this.attach (list_box, 0, this.last_row, 3, 1);
     this.last_row++;
   }
 
   private void update () {
     this.last_row = 0;
-    this.foreach ((child) => this.remove (child));
 
-    var image_frame = new Avatar (PROFILE_SIZE, this.individual);
-    image_frame.set_vexpand (false);
-    image_frame.set_valign (Gtk.Align.START);
-
-    this.attach (image_frame,  0, 0, 1, 3);
+    // Remove all fields
+    unowned var child = get_first_child ();
+    while (child != null) {
+      unowned var next = child.get_next_sibling ();
+      remove (child);
+      child = next;
+    }
 
-    create_name_label ();
+    var header = create_header ();
+    this.attach (header, 0, 0, 1, 1);
 
-    this.last_row += 3; // Name/Avatar takes up 3 rows
+    this.last_row++;
 
     var personas = Utils.get_personas_for_display (this.individual);
     /* Cause personas are sorted properly I can do this */
-    foreach (var p in personas) {
-      bool is_first_persona = (this.last_row == 3);
+    for (int i = 0; i < personas.get_n_items (); i++) {
+      var p = (Persona) personas.get_item (i);
       int persona_store_pos = this.last_row;
-      if (!is_first_persona) {
+
+      if (i > 0) {
         this.attach (create_persona_store_label (p), 0, this.last_row, 3);
         this.last_row++;
       }
 
-      foreach (var prop in SORTED_PROPERTIES)
+      foreach (unowned var prop in SORTED_PROPERTIES)
         add_row_for_property (p, prop);
 
       // Nothing to show in the persona: don't mention it
       bool is_empty_persona = (this.last_row == persona_store_pos + 1);
-      if (!is_first_persona && is_empty_persona) {
+      if (i > 0 && is_empty_persona) {
         this.remove_row (persona_store_pos);
         this.last_row--;
       }
     }
-
-    show_all ();
   }
 
-  private void update_name_label (Gtk.Label name_label) {
-    var name = Markup.printf_escaped ("<span font='16'>%s</span>",
-                                      this.individual.display_name);
-    name_label.set_markup (name);
-  }
+  private Gtk.Widget create_header () {
+    var header = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 18);
+    header.add_css_class ("contacts-sheet-header");
+
+    var image_frame = new Avatar (PROFILE_SIZE, this.individual);
+    image_frame.vexpand = false;
+    image_frame.valign = Gtk.Align.START;
+    header.append (image_frame);
 
-  private void create_name_label () {
     var name_label = new Gtk.Label ("");
-    name_label.ellipsize = Pango.EllipsizeMode.END;
+    name_label.set_markup (this.individual.display_name);
+    name_label.hexpand = true;
     name_label.xalign = 0f;
+    name_label.wrap = true;
     name_label.lines = 4;
+    name_label.width_chars = 10;
     name_label.selectable = true;
-    name_label.set_can_focus (false);
-    this.attach (name_label,  1, 0, 1, 3);
-    update_name_label (name_label);
-    this.individual.notify["display-name"].connect ((obj, spec) => {
-      update_name_label (name_label);
-    });
+    name_label.can_focus = false;
+    name_label.add_css_class ("title-1");
+    header.append (name_label);
+
+    return header;
   }
 
   private void add_row_for_property (Persona persona, string property) {
     switch (property) {
       case "email-addresses":
-        add_emails (persona);
+        add_emails (persona, property);
         break;
       case "phone-numbers":
-        add_phone_nrs (persona);
+        add_phone_nrs (persona, property);
         break;
       case "im-addresses":
-        add_im_addresses (persona);
+        add_im_addresses (persona, property);
         break;
       case "urls":
-        add_urls (persona);
+        add_urls (persona, property);
         break;
       case "nickname":
-        add_nickname (persona);
+        add_nickname (persona, property);
         break;
       case "birthday":
-        add_birthday (persona);
+        add_birthday (persona, property);
         break;
       case "notes":
-        add_notes (persona);
+        add_notes (persona, property);
         break;
       case "postal-addresses":
-        add_postal_addresses (persona);
+        add_postal_addresses (persona, property);
         break;
       default:
         debug ("Unsupported property: %s", property);
@@ -197,97 +198,114 @@ public class Contacts.ContactSheet : Gtk.Grid {
     }
   }
 
-  private void add_emails (Persona persona) {
-    var details = persona as EmailDetails;
-    if (details != null) {
-      var emails = Utils.sort_fields<EmailFieldDetails>(details.email_addresses);
-      foreach (var email in emails) {
-        var button = create_button ("mail-unread-symbolic");
-        button.clicked.connect (() => {
-          Utils.compose_mail ("%s <%s>".printf(this.individual.display_name, email.value));
-        });
-        add_row_with_label (TypeSet.email.format_type (email), email.value, button);
-      }
+  private void add_emails (Persona persona, string property) {
+    unowned var details = persona as EmailDetails;
+    if (details == null)
+      return;
+
+    var emails = Utils.sort_fields<EmailFieldDetails>(details.email_addresses);
+    foreach (var email in emails) {
+      if (email.value == "")
+        continue;
+
+      var row = new ContactSheetRow (property,
+                                     email.value,
+                                     TypeSet.email.format_type (email));
+
+      var button = row.add_button ("mail-send-symbolic");
+      button.tooltip_text = _("Send an email to %s".printf (email.value));
+      button.clicked.connect (() => {
+        Utils.compose_mail ("%s <%s>".printf(this.individual.display_name, email.value));
+      });
+
+      this.attach_row (row);
     }
   }
 
-  private void add_phone_nrs (Persona persona) {
-    var phone_details = persona as PhoneDetails;
-    if (phone_details != null) {
-      var phones = Utils.sort_fields<PhoneFieldDetails>(phone_details.phone_numbers);
-      foreach (var phone in phones) {
+  private void add_phone_nrs (Persona persona, string property) {
+    unowned var phone_details = persona as PhoneDetails;
+    if (phone_details == null)
+      return;
+
+    var phones = Utils.sort_fields<PhoneFieldDetails>(phone_details.phone_numbers);
+    foreach (var phone in phones) {
+      if (phone.value == "")
+        continue;
+
+      var row = new ContactSheetRow (property,
+                                     phone.value,
+                                     TypeSet.phone.format_type (phone));
+
 #if HAVE_TELEPATHY
-        if (this.store.caller_account != null) {
-          var call_button = create_button ("call-start-symbolic");
-          call_button.clicked.connect (() => {
-            Utils.start_call (phone.value, this.store.caller_account);
-          });
-
-          add_row_with_label (TypeSet.phone.format_type (phone), phone.value, call_button);
-        } else {
-          add_row_with_label (TypeSet.phone.format_type (phone), phone.value);
-        }
-#else
-        add_row_with_label (TypeSet.phone.format_type (phone), phone.value);
-#endif
+      if (this.store.caller_account != null) {
+        var button = row.add_button ("call-start-symbolic");
+        button.tooltip_text = _("Start a call");
+        button.clicked.connect (() => {
+          Utils.start_call (phone.value, this.store.caller_account);
+        });
       }
+#endif
+
+      this.attach_row (row);
     }
   }
 
-  private void add_im_addresses (Persona persona) {
+  private void add_im_addresses (Persona persona, string property) {
 #if HAVE_TELEPATHY
-    var im_details = persona as ImDetails;
-    if (im_details != null) {
-      foreach (var protocol in im_details.im_addresses.get_keys ()) {
-        foreach (var id in im_details.im_addresses[protocol]) {
-          if (persona is Tpf.Persona) {
-            var button = create_button ("user-available-symbolic");
-            button.clicked.connect (() => {
-              var im_persona = Utils.find_im_persona (individual, protocol, id.value);
-              if (im_persona != null) {
-                var type = im_persona.presence_type;
-                if (type != PresenceType.UNSET && type != PresenceType.ERROR &&
-                    type != PresenceType.OFFLINE && type != PresenceType.UNKNOWN) {
-                  Utils.start_chat (this.individual, protocol, id.value);
-                }
-              }
-            });
-            add_row_with_label (ImService.get_display_name (protocol), id.value, button);
+    unowned var im_details = persona as ImDetails;
+    if (im_details == null)
+      return;
+
+    foreach (var protocol in im_details.im_addresses.get_keys ()) {
+      foreach (var id in im_details.im_addresses[protocol]) {
+        if (!(persona is Tpf.Persona))
+          continue;
+
+        var row = new ContactSheetRow (property,
+                                       id.value,
+                                       ImService.get_display_name (protocol));
+        var button = row.add_button ("user-available-symbolic");
+        button.clicked.connect (() => {
+          var im_persona = Utils.find_im_persona (individual, protocol, id.value);
+          if (im_persona != null) {
+            var type = im_persona.presence_type;
+            if (type != PresenceType.UNSET && type != PresenceType.ERROR &&
+                type != PresenceType.OFFLINE && type != PresenceType.UNKNOWN) {
+              Utils.start_chat (this.individual, protocol, id.value);
+            }
           }
-        }
+        });
+        this.attach_row (row);
       }
     }
 #endif
   }
 
-  private void add_urls (Persona persona) {
-    var url_details = persona as UrlDetails;
-    if (url_details != null) {
-      foreach (var url in url_details.urls) {
-        var button = create_button ("web-browser-symbolic");
-        button.clicked.connect (() => {
-          unowned var window = button.get_toplevel () as MainWindow;
-          if (window == null)
-            return;
-
-          try {
-            Gtk.show_uri_on_window (window,
-                                    fallback_to_https (url.value),
-                                    Gdk.CURRENT_TIME);
-          } catch (Error e) {
-            var message = "Failed to open url '%s'".printf(url.value);
-
-            // Notify the user
-            var notification = new InAppNotification (message);
-            notification.show ();
-            window.add_notification (notification);
-
-            // Print details on stdout
-            debug (message + ": " + e.message);
-          }
-        });
-        add_row_with_label (_("Website"), url.value, button);
-      }
+  private void add_urls (Persona persona, string property) {
+    unowned var url_details = persona as UrlDetails;
+    if (url_details == null)
+      return;
+
+    foreach (var url in url_details.urls) {
+      if (url.value == "")
+        continue;
+
+      var row = new ContactSheetRow (property, url.value);
+
+      var button = row.add_button ("external-link-symbolic");
+      button.tooltip_text = _("Visit website");
+      button.clicked.connect (() => {
+        unowned var window = button.get_root () as MainWindow;
+        if (window == null)
+          return;
+
+        // FIXME: use show_uri_full so we can show errors
+        Gtk.show_uri (window,
+                      fallback_to_https (url.value),
+                      Gdk.CURRENT_TIME);
+      });
+
+      this.attach_row (row);
     }
   }
 
@@ -300,33 +318,52 @@ public class Contacts.ContactSheet : Gtk.Grid {
     return url;
   }
 
-  private void add_nickname (Persona persona) {
-    var name_details = persona as NameDetails;
-    if (name_details != null && is_set (name_details.nickname))
-      add_row_with_label (_("Nickname"), name_details.nickname);
+  private void add_nickname (Persona persona, string property) {
+    unowned var name_details = persona as NameDetails;
+    if (name_details == null || name_details.nickname == "")
+      return;
+
+    var row = new ContactSheetRow (property, name_details.nickname);
+    this.attach_row (row);
   }
 
-  private void add_birthday (Persona persona) {
-    var birthday_details = persona as BirthdayDetails;
-    if (birthday_details != null && birthday_details.birthday != null)
-      add_row_with_label (_("Birthday"), birthday_details.birthday.to_local ().format ("%x"));
+  private void add_birthday (Persona persona, string property) {
+    unowned var birthday_details = persona as BirthdayDetails;
+    if (birthday_details == null || birthday_details.birthday == null)
+      return;
+
+    var birthday_str = birthday_details.birthday.to_local ().format ("%x");
+    var row = new ContactSheetRow (property, birthday_str);
+    this.attach_row (row);
   }
 
-  private void add_notes (Persona persona) {
-    var note_details = persona as NoteDetails;
-    if (note_details != null) {
-      foreach (var note in note_details.notes)
-        add_row_with_label (_("Note"), note.value);
+  private void add_notes (Persona persona, string property) {
+    unowned var note_details = persona as NoteDetails;
+    if (note_details == null)
+      return;
+
+    foreach (var note in note_details.notes) {
+      if (note.value == "")
+        continue;
+
+      var row = new ContactSheetRow (property, note.value);
+      this.attach_row (row);
     }
   }
 
-  private void add_postal_addresses (Persona persona) {
-    var addr_details = persona as PostalAddressDetails;
-    if (addr_details != null) {
-      foreach (var addr in addr_details.postal_addresses) {
-        var all_strs = string.joinv ("\n", Utils.format_address (addr.value));
-        add_row_with_label (TypeSet.general.format_type (addr), all_strs);
-      }
+  private void add_postal_addresses (Persona persona, string property) {
+    unowned var addr_details = persona as PostalAddressDetails;
+    if (addr_details == null)
+      return;
+
+    foreach (var addr in addr_details.postal_addresses) {
+      if (addr.value.is_empty ())
+        continue;
+
+      var row = new ContactSheetRow (property,
+                                     string.joinv ("\n", Utils.format_address (addr.value)),
+                                     TypeSet.general.format_type (addr));
+      this.attach_row (row);
     }
   }
 }
diff --git a/src/contacts-crop-dialog.vala b/src/contacts-crop-dialog.vala
new file mode 100644
index 00000000..0a9cbb21
--- /dev/null
+++ b/src/contacts-crop-dialog.vala
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2018 Elias Entrup <elias-git flump de>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+[GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-crop-dialog.ui")]
+public class Contacts.CropDialog : Gtk.Dialog {
+
+  [GtkChild]
+  private unowned Gtk.Box box;
+
+  private Cc.CropArea crop_area;
+
+  construct {
+    this.crop_area = new Cc.CropArea ();
+    this.crop_area.vexpand = true;
+    this.crop_area.hexpand = true;
+    this.crop_area.set_min_size (48, 48);
+    this.box.append (this.crop_area);
+  }
+
+  public CropDialog.for_pixbuf (Gdk.Pixbuf pixbuf, Gtk.Window? parent = null) {
+    Object (use_header_bar: 1, transient_for: parent);
+
+    this.crop_area.set_paintable (Gdk.Texture.for_pixbuf (pixbuf));
+  }
+
+  public Gdk.Pixbuf create_pixbuf () {
+    return this.crop_area.create_pixbuf ();
+  }
+}
diff --git a/src/contacts-delete-operation.vala b/src/contacts-delete-operation.vala
new file mode 100644
index 00000000..022fff5e
--- /dev/null
+++ b/src/contacts-delete-operation.vala
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+public class Contacts.DeleteOperation : Object, Operation {
+
+  private Gee.List<Individual> individuals;
+
+  // We don't support reversing a removal. What we do instead, is put a timeout
+  // before actually executing this operation so the user has time to change
+  // their mind.
+  public bool reversable { get { return false; } }
+
+  private string _description;
+  public string description { owned get { return this._description; } }
+
+  public DeleteOperation (Gee.List<Individual> individuals) {
+    this.individuals = individuals;
+    this._description = ngettext ("Deleting %d contact",
+                                  "Deleting %d contacts", individuals.size)
+                        .printf (individuals.size);
+  }
+
+  /**
+   * Link individuals
+   */
+  public async void execute () throws GLib.Error {
+    foreach (var indiv in this.individuals) {
+      foreach (var persona in indiv.personas) {
+        // TODO: make sure it is actually removed
+        yield persona.store.remove_persona (persona);
+      }
+    }
+  }
+
+  // See comments near the reversable property
+  protected async void _undo () throws GLib.Error {
+    throw new GLib.IOError.NOT_SUPPORTED ("Undoing not supported");
+  }
+}
diff --git a/src/contacts-editor-persona.vala b/src/contacts-editor-persona.vala
index f0888a07..bdb506f8 100644
--- a/src/contacts-editor-persona.vala
+++ b/src/contacts-editor-persona.vala
@@ -1,6 +1,7 @@
 /*
  * Copyright (C) 2019 Purism SPC
  * Author: Julian Sparber <julian sparber puri sm>
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -37,19 +38,21 @@ public class Contacts.EditorPersona : Gtk.Box {
     "notes"
   };
 
-  private Folks.Persona persona;
-  private Gtk.Box header;
-  private Gtk.ListBox content;
+  private unowned Folks.Persona persona;
+  private unowned Gtk.Box header;
+  private unowned Gtk.Box content;
 
-  private Folks.IndividualAggregator aggregator;
+  private unowned Folks.IndividualAggregator aggregator;
 
   construct {
-    this.header = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
-    add (this.header);
+    var _header = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
+    this.append (_header);
+    this.header = _header;
 
-    this.content = new Gtk.ListBox ();
-    this.content.get_style_context ().add_class ("content");
-    add (this.content);
+    var listbox = new Gtk.Box (Gtk.Orientation.VERTICAL, 0);
+    this.content = listbox;
+    this.content.add_css_class ("boxed-list");
+    this.append (this.content);
   }
 
   public EditorPersona (Persona persona, IndividualAggregator aggregator) {
@@ -57,45 +60,48 @@ public class Contacts.EditorPersona : Gtk.Box {
     this.persona = persona;
     this.aggregator = aggregator;
     create_label ();
-    // TODO: implement the possibility of changing the addressbook of a persona
+    // TODO: implement the possibility f changing the addressbook of a persona
 
     // Add most important properites
-    foreach (var property in PROPERTIES) {
+    foreach (unowned var property in PROPERTIES) {
       debug ("Create property entry for %s", property);
-      var rows = new EditorProperty (persona, property);
-      foreach (var row in rows) {
+      var prop_editor = new EditorProperty (persona, property);
+
+      for (int i = 0; i < prop_editor.get_n_items (); i++) {
+        var row = (EditorPropertyRow) prop_editor.get_item (i);
         row.show_with_animation (false);
         connect_row (row);
-        this.content.add (row);
+        this.content.append (row);
       }
     }
-    // Add a row with a button to show all properties
-    Gtk.ListBoxRow show_all_row = new Gtk.ListBoxRow ();
-    show_all_row.set_selectable (false);
-    // Add less important property when the show_more button is clicked
-    this.content.row_activated.connect ((current_row) => {
-      if (current_row == show_all_row) {
-        foreach (var property in OTHER_PROPERTIES) {
-          debug ("Create property entry for %s", property);
-          var rows = new EditorProperty (persona, property);
-          foreach (var row in rows) {
-            connect_row (row);
-            this.content.add (row);
-            row.show_with_animation ();
-          }
+
+    // Add less important properties when the show_more button is clicked
+    var show_more_button = new Gtk.Button ();
+    var show_more_content = new Adw.ButtonContent ();
+    show_more_content.icon_name = "view-more-symbolic";
+    show_more_content.label = _("Show More");
+    show_more_button.set_child (show_more_content);
+    show_more_button.halign = Gtk.Align.CENTER;
+    show_more_button.add_css_class ("flat");
+    show_more_button.clicked.connect ((current_row) => {
+      foreach (unowned string property in OTHER_PROPERTIES) {
+        debug ("Create property entry for %s", property);
+        var prop_editor = new EditorProperty (persona, property);
+
+        for (int i = 0; i < prop_editor.get_n_items (); i++) {
+          var row = (EditorPropertyRow) prop_editor.get_item (i);
+          connect_row (row);
+          this.content.append (row);
+          row.show_with_animation ();
         }
-        show_all_row.destroy ();
       }
+      this.content.remove (show_more_button);
     });
-    Gtk.Image show_all = new Gtk.Image.from_icon_name ("view-more-symbolic",
-                                                       Gtk.IconSize.BUTTON);
-    show_all.margin = 12;
-    show_all_row.add (show_all);
-    this.content.add (show_all_row);
+    this.content.append (show_more_button);
   }
 
   private void connect_row (EditorPropertyRow row) {
-    row.notify["is-empty"].connect ( () => {
+    row.notify["is-empty"].connect (() => {
       var empty_rows_count = this.count_empty_rows (row.ptype);
       if (row.is_empty) {
         // destroy all rows of our type which is not us
@@ -104,10 +110,10 @@ public class Contacts.EditorPersona : Gtk.Box {
       if (!row.is_empty && empty_rows_count == 0) {
         // We are sure that we only created one new row
         var new_rows = new EditorProperty (persona, row.ptype, true);
-        if (new_rows.size > 0) {
-          this.content.insert (new_rows[0], row.get_index () + 1);
-          connect_row (new_rows[0]);
-          new_rows[0].show_with_animation ();
+        if (new_rows.get_n_items () > 0) {
+          var first_row = (EditorPropertyRow) new_rows.get_item (0);
+          this.content.insert_child_after (first_row, row);
+          connect_row (first_row);
         } else {
           debug ("Couldn't add new row with type %s", row.ptype);
         }
@@ -117,8 +123,10 @@ public class Contacts.EditorPersona : Gtk.Box {
 
   private uint count_empty_rows (string type) {
     uint count = 0;
-    foreach (var row in this.content.get_children ()) {
-      var prop = (row as EditorPropertyRow);
+    for (unowned Gtk.Widget? child = this.content.get_first_child ();
+         child != null;
+         child = child.get_next_sibling ()) {
+      unowned var prop = (child as EditorPropertyRow);
       if (prop != null && !prop.is_removed && prop.is_empty && prop.ptype == type) {
         count++;
       }
@@ -126,20 +134,23 @@ public class Contacts.EditorPersona : Gtk.Box {
     return count;
   }
 
-  private void destroy_empty_rows (Gtk.ListBoxRow current_row, string type) {
-    foreach (var row in this.content.get_children ()) {
-      if (current_row != row) {
-        var prop = (row as EditorPropertyRow);
-        if (prop != null && !prop.is_removed && prop.is_empty && prop.ptype == type) {
-          prop.remove ();
-        }
+  private void destroy_empty_rows (Gtk.Widget current_row, string type) {
+    for (unowned Gtk.Widget? child = this.content.get_first_child ();
+         child != null;
+         child = child.get_next_sibling ()) {
+      if (current_row == child)
+        continue;
+
+      unowned var prop = (child as EditorPropertyRow);
+      if (prop != null && !prop.is_removed && prop.is_empty && prop.ptype == type) {
+        prop.remove ();
       }
     }
   }
 
   private void create_label () {
     string title = "";
-    FakePersona fake_persona = this.persona as FakePersona;
+    unowned var fake_persona = this.persona as FakePersona;
     if (fake_persona != null && fake_persona.real_persona != null) {
       title = fake_persona.real_persona.store.display_name;
     } else {
@@ -147,7 +158,7 @@ public class Contacts.EditorPersona : Gtk.Box {
     }
 
     Gtk.Label addressbook = new Gtk.Label (title);
-    addressbook.get_style_context ().add_class ("heading");
-    this.header.pack_start (addressbook, false, false, 0);
+    addressbook.add_css_class ("heading");
+    this.header.append (addressbook);
   }
 }
diff --git a/src/contacts-editor-property.vala b/src/contacts-editor-property.vala
index d03cde3b..8be766ff 100644
--- a/src/contacts-editor-property.vala
+++ b/src/contacts-editor-property.vala
@@ -1,6 +1,7 @@
 /*
  * Copyright (C) 2019 Purism SPC
  * Author: Julian Sparber <julian sparber puri sm>
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -19,71 +20,65 @@
 using Folks;
 
 public class Contacts.BirthdayEditor : Gtk.Dialog {
-  private Gtk.SpinButton day_spin;
-  private Gtk.ComboBoxText month_combo;
-  private Gtk.SpinButton year_spin;
-  public bool is_set { get; set; default = false; }
 
-  public signal void changed ();
-  delegate void AdjustingDateFn ();
+  private unowned Gtk.SpinButton day_spin;
+  private unowned Gtk.ComboBoxText month_combo;
+  private unowned Gtk.SpinButton year_spin;
 
-  public GLib.DateTime get_birthday () {
-    return new GLib.DateTime.local (year_spin.get_value_as_int (),
-                                    month_combo.get_active () + 1,
-                                    day_spin.get_value_as_int (),
-                                    0, 0, 0).to_utc ();
-  }
+  public bool is_set { get; set; default = false; }
 
-  public BirthdayEditor (Gtk.Window window, DateTime birthday) {
-    Object (transient_for: window, use_header_bar: 1);
-    day_spin = new Gtk.SpinButton.with_range (1.0, 31.0, 1.0);
-    day_spin.set_digits (0);
-    day_spin.numeric = true;
-    day_spin.set_value ((double)birthday.to_local ().get_day_of_month ());
+  public signal void changed ();
 
-    month_combo = new Gtk.ComboBoxText ();
+  construct {
+    // The grid that will contain the Y/M/D fields
+    var grid = new Gtk.Grid ();
+    grid.column_spacing = 12;
+    grid.row_spacing = 12;
+    grid.add_css_class ("contacts-editor-birthday");
+    ((Gtk.Box) this.get_content_area ()).append (grid);
+
+    // Day
+    var d_spin = new Gtk.SpinButton.with_range (1.0, 31.0, 1.0);
+    d_spin.digits = 0;
+    d_spin.numeric = true;
+    this.day_spin = d_spin;
+
+    // Month
+    var m_combo = new Gtk.ComboBoxText ();
     var january = new DateTime.local (1, 1, 1, 1, 1, 1);
     for (int i = 0; i < 12; i++) {
       var month = january.add_months (i);
-      month_combo.append_text (month.format ("%B"));
+      m_combo.append_text (month.format ("%B"));
     }
-    month_combo.set_active (birthday.to_local ().get_month () - 1);
-    month_combo.hexpand = true;
+    m_combo.hexpand = true;
+    this.month_combo = m_combo;
 
-    year_spin = new Gtk.SpinButton.with_range (1800, 3000, 1);
-    year_spin.set_digits (0);
-    year_spin.numeric = true;
-    year_spin.set_value ((double)birthday.to_local ().get_year ());
+    // Year
+    var y_spin = new Gtk.SpinButton.with_range (1800, 3000, 1);
+    y_spin.set_digits (0);
+    y_spin.numeric = true;
+    this.year_spin = y_spin;
 
     // Create grid and labels
-    var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 12);
-    var grid = new Gtk.Grid ();
-    grid.set_column_spacing (12);
-    grid.set_row_spacing (12);
-    Gtk.Label day = new Gtk.Label(_("Day"));
+    Gtk.Label day = new Gtk.Label (_("Day"));
     day.set_halign (Gtk.Align.END);
     grid.attach (day, 0, 0);
     grid.attach (day_spin, 1, 0);
-    Gtk.Label month = new Gtk.Label(_("Month"));
+    Gtk.Label month = new Gtk.Label (_("Month"));
     month.set_halign (Gtk.Align.END);
     grid.attach (month, 0, 1);
     grid.attach (month_combo, 1, 1);
-    Gtk.Label year = new Gtk.Label(_("Year"));
+    Gtk.Label year = new Gtk.Label (_("Year"));
     year.set_halign (Gtk.Align.END);
     grid.attach (year, 0, 2);
     grid.attach (year_spin, 1, 2);
-    box.pack_start (grid);
-
-    var content = this.get_content_area ();
-    content.set_valign (Gtk.Align.CENTER);
-    content.add (box);
 
     this.title = _("Change Birthday");
     add_buttons (_("Set"), Gtk.ResponseType.OK,
                  _("Cancel"), Gtk.ResponseType.CANCEL,
                  null);
     var ok_button = this.get_widget_for_response (Gtk.ResponseType.OK);
-    ok_button.get_style_context ().add_class ("suggested-action");
+    ok_button.add_css_class ("suggested-action");
     this.response.connect ((id) => {
       switch (id) {
         case Gtk.ResponseType.OK:
@@ -95,39 +90,48 @@ public class Contacts.BirthdayEditor : Gtk.Dialog {
       }
       this.destroy ();
     });
+  }
 
-    box.margin = 12;
-    box.show_all ();
-
-    AdjustingDateFn fn = () => {
-      int[] month_of_31 = {3, 5, 8, 10};
-      if (month_combo.get_active () in month_of_31) {
-        day_spin.set_range (1, 30);
-      } else if (month_combo.get_active () == 1) {
-        if (year_spin.get_value_as_int () % 400 == 0 ||
-            (year_spin.get_value_as_int () % 4 == 0 &&
-             year_spin.get_value_as_int () % 100 != 0)) {
-          day_spin.set_range (1, 29);
-        } else {
-          day_spin.set_range (1, 28);
-        }
-      } else {
-        day_spin.set_range (1, 31);
-      }
-    };
+  public BirthdayEditor (Gtk.Window window, DateTime birthday) {
+    Object (transient_for: window, use_header_bar: 1);
 
-    /* adjusting day_spin value using selected month/year constraints*/
-    fn ();
+    this.day_spin.set_value ((double) birthday.get_day_of_month ());
+    this.month_combo.set_active (birthday.get_month () - 1);
+    this.year_spin.set_value ((double) birthday.get_year ());
 
+    update_date ();
     month_combo.changed.connect (() => {
-      /* adjusting day_spin value using selected month constraints*/
-      fn ();
+      update_date ();
     });
     year_spin.value_changed.connect (() => {
-      /* adjusting day_spin value using selected year constraints*/
-      fn ();
+      update_date ();
     });
   }
+
+  public GLib.DateTime get_birthday () {
+    return new GLib.DateTime.local (year_spin.get_value_as_int (),
+                                    month_combo.get_active () + 1,
+                                    day_spin.get_value_as_int (),
+                                    0, 0, 0).to_utc ();
+  }
+
+  private void update_date() {
+    const int[] month_of_31 = {3, 5, 8, 10};
+
+    if (this.month_combo.get_active () in month_of_31) {
+      this.day_spin.set_range (1, 30);
+    } else if (this.month_combo.get_active () == 1) {
+      if (this.year_spin.get_value_as_int () % 400 == 0 ||
+          (this.year_spin.get_value_as_int () % 4 == 0 &&
+           this.year_spin.get_value_as_int () % 100 != 0)) {
+        this.day_spin.set_range (1, 29);
+      } else {
+        this.day_spin.set_range (1, 28);
+      }
+    } else {
+      this.day_spin.set_range (1, 31);
+    }
+  }
 }
 
 public class Contacts.AddressEditor : Gtk.Box {
@@ -138,28 +142,31 @@ public class Contacts.AddressEditor : Gtk.Box {
 
   public signal void changed ();
 
-  public AddressEditor (PostalAddressFieldDetails details) {
-    set_hexpand (true);
-    set_orientation (Gtk.Orientation.VERTICAL);
+  construct {
+    this.add_css_class ("contacts-editor-address");
+
+    this.hexpand = true;
+    this.orientation = Gtk.Orientation.VERTICAL;
+  }
 
+  public AddressEditor (PostalAddressFieldDetails details) {
     for (int i = 0; i < entries.length; i++) {
       string postal_part;
       details.value.get (AddressEditor.postal_element_props[i], out postal_part);
 
-      entries[i] = new Gtk.Entry ();
-      entries[i].set_hexpand (true);
-      entries[i].set ("placeholder-text", AddressEditor.postal_element_names[i]);
+      this.entries[i] = new Gtk.Entry ();
+      this.entries[i].hexpand = true;
+      this.entries[i].placeholder_text = AddressEditor.postal_element_names[i];
+      this.entries[i].add_css_class ("flat");
 
       if (postal_part != null)
-        entries[i].set_text (postal_part);
+        this.entries[i].text = postal_part;
 
-      entries[i].get_style_context ().add_class ("contacts-postal-entry");
-      add (entries[i]);
+      append (this.entries[i]);
 
-      var entry = entries[i];
       var prop_name = AddressEditor.postal_element_props[i];
       entries[i].changed.connect (() => {
-        details.value.set (prop_name, entry.get_text ());
+        details.value.set (prop_name, this.entries[i].text);
         changed ();
       });
     }
@@ -173,58 +180,45 @@ public class Contacts.AddressEditor : Gtk.Box {
     }
     return true;
   }
-
-  public override void grab_focus () {
-    entries[0].grab_focus ();
-  }
 }
 
-public class Contacts.EditorPropertyRow : Gtk.ListBoxRow {
+/**
+ * Basic widget to show a single property of a contact (for example an email
+ * address, a birthday, ...). It can show itself using a GtkRevealer animation.
+ *
+ * To edit the value of the property, you should supply a widget and set it as
+ * the main widget.
+ */
+public class Contacts.EditorPropertyRow : Adw.Bin {
+
+  private unowned Gtk.Revealer revealer;
+  private unowned Gtk.ListBox listbox;
+
   public bool is_empty { get; set; default = true; }
   public bool is_removed { get; set; default = false; }
-  public string ptype { get; private set; }
-  public Gtk.Box container;
-  public Gtk.Box header;
-  public Gtk.Revealer revealer;
+  public bool removable { get; set; default = false; }
+
+  /** Internal type name of the property */
+  public string ptype { get; construct; }
 
   construct {
-    this.revealer = new Gtk.Revealer ();
-    //TODO: bind orientation property to available space
-    var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 6);
-    box.set_valign (Gtk.Align.START);
-    box.set_can_focus (false);
-    this.container = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6);
-    this.container.set_can_focus (false);
-    this.header = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 6);
-    this.header.set_can_focus (false);
-    box.pack_start (this.header);
-    box.pack_end (this.container);
-    this.set_activatable (false);
-    this.set_selectable (false);
-    this.set_can_focus (false);
-    box.margin = 12;
-    this.revealer.add (box);
-    add (this.revealer);
-    this.get_style_context ().add_class ("editor-property-row");
-    this.revealer.bind_property ("reveal-child", this, "is-removed", BindingFlags.INVERT_BOOLEAN);
+    var _revealer = new Gtk.Revealer ();
+    _revealer.bind_property ("reveal-child", this, "is-removed",
+                             BindingFlags.BIDIRECTIONAL | BindingFlags.INVERT_BOOLEAN);
+    this.child = _revealer;
+    this.revealer = _revealer;
+
+    var list_box = new Gtk.ListBox ();
+    this.listbox = list_box;
+    this.listbox.selection_mode = Gtk.SelectionMode.NONE;
+    this.listbox.activate_on_single_click = true;
+    this.listbox.add_css_class ("boxed-list");
+    this.listbox.add_css_class ("contacts-editor-property");
+    this.revealer.set_child (listbox);
   }
 
   public EditorPropertyRow (string type) {
-    this.ptype = type;
-  }
-
-  // This hides the widget with an animation and then destroys it
-  public new void remove () {
-    this.revealer.set_reveal_child (false);
-    // Remove the separator during the animation to make it look a little better
-    Timeout.add (this.revealer.get_transition_duration ()/2, () => {
-      this.set_header (null);
-      return false;
-    });
-
-    this.revealer.notify["child-revealed"].connect ( () => {
-      this.destroy ();
-    });
+    Object (ptype: type);
   }
 
   public void show_with_animation (bool animate = true) {
@@ -232,133 +226,136 @@ public class Contacts.EditorPropertyRow : Gtk.ListBoxRow {
       var duration = this.revealer.get_transition_duration ();
       this.revealer.set_reveal_child (true);
       this.revealer.set_transition_duration (duration);
-      this.show_all ();
     } else {
-      this.show_all ();
       this.revealer.set_reveal_child (true);
     }
   }
 
-  public void add_base_label (string label) {
-    var title_label = new Gtk.Label (label);
-    title_label.set_hexpand (false);
-    title_label.set_halign (Gtk.Align.START);
-    title_label.margin_end = 6;
-    this.header.pack_start (title_label);
-  }
+  // This hides the widget with an animation and then destroys it
+  public void remove () {
+    debug ("Property %s is removed", this.ptype);
 
-  public void add_base_combo (Gee.Set<AbstractFieldDetails> details_set,
-                              string label,
-                              TypeSet combo_type,
-                              AbstractFieldDetails details) {
-    var title_label = new Gtk.Label (label);
-    title_label.set_halign (Gtk.Align.START);
-    this.header.pack_start (title_label);
-    TypeCombo combo = new TypeCombo (combo_type);
-    combo.set_hexpand (false);
-    combo.set_active_from_field_details (details);
-    this.header.pack_start (combo);
-
-    combo.changed.connect (() => {
-      combo.active_descriptor.save_to_field_details(details);
-      // Workaround: we shouldn't do a manual signal
-      ((FakeHashSet) details_set).changed ();
-      debug ("Property phone changed");
+    this.revealer.set_reveal_child (false);
+
+    // Remove the separator during the animation to make it look a little better
+    Timeout.add (this.revealer.get_transition_duration ()/2, () => {
+      return false;
     });
-  }
 
-  //FIXME: create only one add_base_entry
-  public void add_base_entry_email (Gee.Set<AbstractFieldDetails> details_set,
-                                    EmailFieldDetails details,
-                                    string placeholder) {
-    var value_entry = new Gtk.Entry ();
-    value_entry.set_input_purpose (Gtk.InputPurpose.EMAIL);
-    value_entry.placeholder_text = placeholder;
-    value_entry.set_text (details.value);
-    value_entry.set_hexpand (true);
-    this.container.pack_start (value_entry);
-
-    this.is_empty = details.value == "";
-
-    value_entry.changed.connect (() => {
-      details.value = value_entry.get_text ();
-      // Workaround: we shouldn't do a manual signal
-      ((FakeHashSet) details_set).changed ();
-      debug ("Property email changed");
-      this.is_empty = value_entry.get_text () == "";
+    this.revealer.notify["child-revealed"].connect (() => {
+      this.destroy ();
     });
   }
 
-  public void add_base_entry_phone (Gee.Set<AbstractFieldDetails> details_set,
-                                    PhoneFieldDetails details,
-                                    string placeholder) {
-    var value_entry = new Gtk.Entry ();
-    value_entry.set_input_purpose (Gtk.InputPurpose.PHONE);
-    value_entry.placeholder_text = placeholder;
-    value_entry.set_text (details.value);
-    value_entry.set_hexpand (true);
-    this.container.pack_start (value_entry);
-
-    this.is_empty = details.value == "";
-
-    value_entry.changed.connect (() => {
-      details.value = value_entry.get_text ();
-      // Workaround: we shouldn't do a manual signal
-      ((FakeHashSet) details_set).changed ();
-      debug ("Property type changed");
+  /**
+   * Setter for the main widget, which can be used to actually edit the property
+   */
+  public void set_main_widget (Gtk.Widget widget, bool add_icon = true) {
+    var row = new Gtk.ListBoxRow ();
+    row.focusable = false;
+
+    var box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0);
+    widget.hexpand = true;
+    row.set_child (box);
+
+    // Start with the icon (if known)
+    if (add_icon) {
+      unowned var icon_name = Utils.get_icon_name_for_property (this.ptype);
+      if (icon_name != null) {
+        var icon = new Gtk.Image.from_icon_name (icon_name);
+        icon.add_css_class ("contacts-property-icon");
+        icon.tooltip_text = Utils.get_display_name_for_property (this.ptype);
+        box.prepend (icon);
+      }
+    }
 
-      this.is_empty = value_entry.get_text () == "";
+    // Set the actual widget
+    // (mimic Adw.ActionRow's "activatable-widget")
+    box.append (widget);
+    this.listbox.row_activated.connect ((activated_row) => {
+      if (row == activated_row)
+        widget.mnemonic_activate (false);
     });
-  }
 
-  public void add_base_entry_url (Gee.Set<AbstractFieldDetails> details_set,
-                                  UrlFieldDetails details,
-                                  string placeholder) {
-    var value_entry = new Gtk.Entry ();
-    value_entry.placeholder_text = placeholder;
-    value_entry.set_input_purpose (Gtk.InputPurpose.URL);
-    value_entry.set_text (details.value);
-    value_entry.set_hexpand (true);
-    this.container.pack_start (value_entry);
+    // Add a delete buton if needed
+    if (this.removable) {
+      var delete_button = new Gtk.Button.from_icon_name ("user-trash-symbolic");
+      delete_button.tooltip_text = _("Delete field");
+      this.bind_property ("is-empty", delete_button, "sensitive", BindingFlags.SYNC_CREATE | 
BindingFlags.INVERT_BOOLEAN);
 
-    this.is_empty = details.value == "";
+      delete_button.clicked.connect ((b) => { this.remove (); });
 
-    value_entry.changed.connect (() => {
-      details.value = value_entry.get_text ();
-      // Workaround: we shouldn't do a manual signal
-      ((FakeHashSet) details_set).changed ();
-      debug ("Property type changed");
+      box.append (delete_button);
+    }
 
-      this.is_empty = value_entry.get_text () == "";
-    });
+    this.listbox.append (row);
   }
 
-  public void add_base_delete (Gee.Set<AbstractFieldDetails> details_set,
-                               AbstractFieldDetails details) {
-    var delete_button = new Gtk.Button.from_icon_name ("user-trash-symbolic");
-    delete_button.get_accessible ().set_name (_("Delete field"));
-    delete_button.set_valign (Gtk.Align.START);
-    this.bind_property ("is-empty", delete_button, "sensitive", BindingFlags.SYNC_CREATE | 
BindingFlags.INVERT_BOOLEAN);
-    this.container.pack_end (delete_button, false);
+  /**
+   * Wrapper around set_main_widget() with some extra styling for GtkEntries,
+   * as well as making sure the "is-empty" property is updated.
+   */
+  public Gtk.Entry set_main_entry (string text, string? placeholder = null) {
+    var entry = new Gtk.Entry ();
+    entry.text = text;
+    entry.placeholder_text = placeholder;
+    entry.add_css_class ("flat");
+    entry.add_css_class ("contacts-editor-main-entry");
+    // Set the icon as part of the GtkEntry, to avoid it being outside of the
+    // margin
+    unowned var icon_name = Utils.get_icon_name_for_property (this.ptype);
+    if (icon_name != null) {
+      entry.primary_icon_name = icon_name;
+      entry.primary_icon_tooltip_text = Utils.get_display_name_for_property (this.ptype);
+    }
+    this.set_main_widget (entry, false);
 
+    this.is_empty = (text == "");
+    entry.changed.connect (() => {
+      this.is_empty = (entry.text == "");
+    });
 
-    delete_button.clicked.connect (() => {
-      debug ("Property removed");
-      this.remove ();
-      details_set.remove (details);
+    return entry;
+  }
+
+  // Adds an extra row for a type combo, to choose between e.g. "Home" or "Work"
+  public void add_type_combo (Gee.Set<AbstractFieldDetails> details_set,
+                              TypeSet combo_type,
+                              AbstractFieldDetails details) {
+    var row = new TypeComboRow (combo_type);
+    row.title = _("Label");
+    row.set_selected_from_field_details (details);
+    this.listbox.append (row);
+
+    row.notify["selected-item"].connect ((obj, pspec) => {
+      unowned var descr = row.selected_descriptor;
+      descr.save_to_field_details (details);
+      // Workaround: we shouldn't do a manual signal
+      ((FakeHashSet) details_set).changed ();
+      debug ("Property phone changed");
     });
   }
 }
 
 /**
  * A widget representing a property of a persona in the editor {@link Contact}.
- * We can have more then one property in one properity e.g. Emails therefore we need to return a List
+ *
+ * We can have more then one field in a single property
+ * (for example: emails, phone nrs, ...), so it implements a
+ * {@link GLib.ListModel}.
  */
-public class Contacts.EditorProperty : Gee.ArrayList<EditorPropertyRow> {
+public class Contacts.EditorProperty : Object, ListModel {
+
+  private GenericArray<EditorPropertyRow> rows;
+
   public bool writeable { get; private set; default = false; }
 
+  construct {
+    this.rows = new GenericArray<EditorPropertyRow> (1);
+  }
+
   public EditorProperty (Persona persona, string property_name, bool only_new = false) {
-    foreach (var s in persona.writeable_properties) {
+    foreach (unowned string s in persona.writeable_properties) {
       if (s == property_name) {
         this.writeable = true;
         break;
@@ -368,185 +365,217 @@ public class Contacts.EditorProperty : Gee.ArrayList<EditorPropertyRow> {
     create_for_property (persona, property_name, only_new);
   }
 
+  public Object? get_item (uint i) {
+    if (i > this.rows.length)
+      return null;
+
+    return this.rows[i];
+  }
+
+  public uint get_n_items () {
+    return this.rows.length;
+  }
+
+  public GLib.Type get_item_type () {
+    return typeof (EditorPropertyRow);
+  }
+
   private void create_for_property (Persona p, string prop_name, bool only_new) {
     switch (prop_name) {
       case "email-addresses":
-        var details = p as EmailDetails;
+        unowned var details = p as EmailDetails;
         if (details != null) {
           var emails = Utils.sort_fields<EmailFieldDetails>(details.email_addresses);
           if (!only_new)
             foreach (var email in emails) {
-              add (create_for_email (details.email_addresses, email));
+              this.rows.add (create_for_email (details.email_addresses, email));
             }
           if (this.writeable)
-            add (create_for_email (details.email_addresses));
+            this.rows.add (create_for_email (details.email_addresses));
         }
         break;
       case "phone-numbers":
-        var details = p as PhoneDetails;
+        unowned var details = p as PhoneDetails;
         if (details != null) {
           var phones = Utils.sort_fields<PhoneFieldDetails>(details.phone_numbers);
           if (!only_new)
             foreach (var phone in phones) {
-              add (create_for_phone (details.phone_numbers, phone));
+              this.rows.add (create_for_phone (details.phone_numbers, phone));
             }
           if (this.writeable)
-            add (create_for_phone (details.phone_numbers));
+            this.rows.add (create_for_phone (details.phone_numbers));
         }
         break;
       case "urls":
-        var details = p as UrlDetails;
+        unowned var details = p as UrlDetails;
         if (details != null) {
           var urls = Utils.sort_fields<UrlFieldDetails>(details.urls);
           if (!only_new)
             foreach (var url in urls) {
-              add (create_for_url (details.urls, url));
+              this.rows.add (create_for_url (details.urls, url));
             }
-          add (create_for_url (details.urls));
+          this.rows.add (create_for_url (details.urls));
         }
         break;
       case "nickname":
-        var name_details = p as NameDetails;
+        unowned var name_details = p as NameDetails;
         if (name_details != null && name_details.nickname != null && !only_new) {
-          add (create_for_nick (name_details));
+          this.rows.add (create_for_nick (name_details));
         }
         break;
       case "birthday":
-        var birthday_details = p as BirthdayDetails;
+        unowned var birthday_details = p as BirthdayDetails;
         if (birthday_details != null && !only_new) {
-          add (create_for_birthday (birthday_details));
+          this.rows.add (create_for_birthday (birthday_details));
         }
         break;
       case "notes":
-        var note_details = p as NoteDetails;
+        unowned var note_details = p as NoteDetails;
         if (note_details != null) {
           if (!only_new)
             foreach (var note in note_details.notes) {
-              add (create_for_note (note_details.notes, note));
+              this.rows.add (create_for_note (note_details.notes, note));
             }
           if (this.writeable)
-            add (create_for_note (note_details.notes));
+            this.rows.add (create_for_note (note_details.notes));
         }
         break;
       case "postal-addresses":
-        var address_details = p as PostalAddressDetails;
+        unowned var address_details = p as PostalAddressDetails;
         if (address_details != null) {
           if (!only_new)
             foreach (var addr in address_details.postal_addresses) {
-              add (create_for_address (address_details.postal_addresses, addr));
+              this.rows.add (create_for_address (address_details.postal_addresses, addr));
             }
           if (this.writeable)
-            add (create_for_address (address_details.postal_addresses));
+            this.rows.add (create_for_address (address_details.postal_addresses));
         }
         break;
     }
   }
 
-  private EditorPropertyRow create_for_email (Gee.Set<AbstractFieldDetails> set,
+  private EditorPropertyRow create_for_email (Gee.Set<AbstractFieldDetails> details_set,
                                               EmailFieldDetails? details = null) {
     if (details == null) {
       var parameters = new Gee.HashMultiMap<string, string> ();
       parameters["type"] = "PERSONAL";
       var new_details = new EmailFieldDetails ("", parameters);
-      set.add(new_details);
+      details_set.add (new_details);
       details = new_details;
     }
-    var box = new EditorPropertyRow ("email-addresses");
-    box.add_base_combo (set, _("Email address"), TypeSet.email, details);
-    box.add_base_entry_email (set, details, _("Add email"));
-    box.add_base_delete (set, details);
 
+    var box = new EditorPropertyRow ("email-addresses");
     box.sensitive = this.writeable;
+
+    var entry = box.set_main_entry (details.value, _("Add email"));
+    entry.set_input_purpose (Gtk.InputPurpose.EMAIL);
+    entry.changed.connect (() => {
+      details.value = entry.get_text ();
+      // Workaround: we shouldn't do a manual signal
+      ((FakeHashSet) details_set).changed ();
+      debug ("Property email changed");
+    });
+
+    box.add_type_combo (details_set, TypeSet.email, details);
+
     return box;
   }
 
-  private EditorPropertyRow create_for_phone (Gee.Set<AbstractFieldDetails> set,
+  private EditorPropertyRow create_for_phone (Gee.Set<AbstractFieldDetails> details_set,
                                               PhoneFieldDetails? details = null) {
     if (details == null) {
       var parameters = new Gee.HashMultiMap<string, string> ();
       parameters["type"] = "CELL";
       var new_details = new PhoneFieldDetails ("", parameters);
-      set.add(new_details);
+      details_set.add (new_details);
       details = new_details;
     }
 
     var box = new EditorPropertyRow ("phone-numbers");
-    box.add_base_combo (set, _("Phone number"), TypeSet.phone, details);
-    box.add_base_entry_phone (set, details, _("Add number"));
-    box.add_base_delete (set, details);
-
     box.sensitive = this.writeable;
+
+    var entry = box.set_main_entry (details.value, _("Add phone number"));
+    entry.set_input_purpose (Gtk.InputPurpose.PHONE);
+    entry.changed.connect (() => {
+      details.value = entry.text;
+      // Workaround: we shouldn't do a manual signal
+      ((FakeHashSet) details_set).changed ();
+      debug ("Property type changed");
+    });
+
+    box.add_type_combo (details_set, TypeSet.phone, details);
+
     return box;
   }
 
   // TODO: add support for different types of urls
-  private EditorPropertyRow create_for_url (Gee.Set<AbstractFieldDetails> set,
+  private EditorPropertyRow create_for_url (Gee.Set<AbstractFieldDetails> details_set,
                                             UrlFieldDetails? details = null) {
     if (details == null) {
       var parameters = new Gee.HashMultiMap<string, string> ();
       parameters["type"] = "PERSONAL";
       var new_details = new UrlFieldDetails ("", parameters);
-      set.add(new_details);
+      details_set.add (new_details);
       details = new_details;
     }
 
     var box = new EditorPropertyRow ("urls");
-    box.add_base_label (_("Website"));
-    box.add_base_entry_url (set, details, _("https://example.com";));
-    box.add_base_delete (set, details);
-
     box.sensitive = this.writeable;
+
+    var entry = box.set_main_entry (details.value, _("https://example.com";));
+    entry.set_input_purpose (Gtk.InputPurpose.URL);
+    entry.changed.connect (() => {
+      details.value = entry.get_text ();
+      // Workaround: we shouldn't do a manual signal
+      ((FakeHashSet) details_set).changed ();
+      debug ("Property type changed");
+    });
+
     return box;
   }
 
   private EditorPropertyRow create_for_nick (NameDetails details) {
     var box = new EditorPropertyRow ("nickname");
-    box.add_base_label (_("Nickname"));
-
-    var value_entry = new Gtk.Entry ();
-    value_entry.set_text (details.nickname);
-    value_entry.set_hexpand (true);
-    box.container.pack_start (value_entry);
+    box.sensitive = this.writeable;
 
-    value_entry.changed.connect (() => {
-      details.nickname = value_entry.get_text ();
+    var entry = box.set_main_entry (details.nickname, _("Nickname"));
+    entry.set_input_purpose (Gtk.InputPurpose.NAME);
+    entry.changed.connect (() => {
+      details.nickname = entry.text;
       debug ("Nickname changed");
-      box.is_empty = value_entry.get_text () == "";
     });
 
-    box.sensitive = this.writeable;
     return box;
   }
 
-  // TODO: support different types of nodes
+  // TODO: support different types of notes
   private EditorPropertyRow create_for_note (Gee.Set<NoteFieldDetails> details_set,
                                              NoteFieldDetails? details = null) {
     if (details == null) {
       var parameters = new Gee.HashMultiMap<string, string> ();
       parameters["type"] = "PERSONAL";
       var new_details = new NoteFieldDetails ("", parameters);
-      details_set.add(new_details);
+      details_set.add (new_details);
       details = new_details;
     }
     var box = new EditorPropertyRow ("notes");
-    box.add_base_label (_("Note"));
 
-    var sw = new Gtk.ScrolledWindow (null, null);
-    sw.set_shadow_type (Gtk.ShadowType.OUT);
+    var sw = new Gtk.ScrolledWindow ();
+    sw.focusable = false;
+    sw.has_frame = false;
     sw.set_size_request (-1, 100);
-    var value_text = new Gtk.TextView ();
-    value_text.get_buffer ().set_text (details.value);
-    value_text.set_hexpand (true);
-    sw.add (value_text);
-    box.container.pack_start (sw);
+    box.set_main_widget (sw);
 
-    box.add_base_delete (details_set, details);
+    var textview = new Gtk.TextView ();
+    textview.get_buffer ().set_text (details.value);
+    textview.hexpand = true;
+    sw.set_child (textview);
 
-    value_text.get_buffer ().changed.connect (() => {
+    textview.get_buffer ().changed.connect (() => {
       Gtk.TextIter start, end;
-      value_text.get_buffer ().get_start_iter (out start);
-      value_text.get_buffer ().get_end_iter (out end);
-      details.value = value_text.get_buffer ().get_text (start, end, true);
+      textview.get_buffer ().get_start_iter (out start);
+      textview.get_buffer ().get_end_iter (out end);
+      details.value = textview.get_buffer ().get_text (start, end, true);
       // Workaround: we shouldn't do a manual signal
       ((FakeHashSet) details_set).changed ();
       debug ("Property changed");
@@ -558,44 +587,41 @@ public class Contacts.EditorProperty : Gee.ArrayList<EditorPropertyRow> {
   }
 
   private EditorPropertyRow create_for_birthday (BirthdayDetails? details) {
-    DateTime date;
+    var date = details.birthday ?? new DateTime.now ();
+
     Gtk.Button button;
     if (details.birthday == null) {
-      date = new DateTime.now ();
       button = new Gtk.Button.with_label (_("Set Birthday"));
     } else {
-      date = details.birthday;
       button = new Gtk.Button.with_label (details.birthday.to_local ().format ("%x"));
     }
 
     var box = new EditorPropertyRow ("birthday");
-    box.add_base_label (_("Birthday"));
-
-    box.container.pack_start (button);
+    box.set_main_widget (button);
 
     button.clicked.connect (() => {
-      var parent_window = button.get_toplevel () as Gtk.Window;
+      unowned var parent_window = button.get_root () as Gtk.Window;
       if (parent_window != null) {
         var dialog = new BirthdayEditor (parent_window, date);
 
         dialog.changed.connect (() => {
           if (dialog.is_set) {
             details.birthday = dialog.get_birthday ();
-            button.set_label (details.birthday.to_local ().format ("%x"));
+            button.set_label (details.birthday.format ("%x"));
             box.is_empty = false;
           }
         });
-        dialog.show_all ();
+        dialog.show ();
       }
     });
 
     box.is_empty = details.birthday == null;
 
     var delete_button = new Gtk.Button.from_icon_name ("user-trash-symbolic");
-    delete_button.get_accessible ().set_name (_("Delete field"));
+    delete_button.tooltip_text = _("Delete field");
     delete_button.set_valign (Gtk.Align.START);
     box.bind_property ("is-empty", delete_button, "sensitive", BindingFlags.SYNC_CREATE | 
BindingFlags.INVERT_BOOLEAN);
-    box.container.pack_end (delete_button, false);
+    // box.container.append (delete_button); XXX
 
     delete_button.clicked.connect (() => {
       debug ("Birthday removed");
@@ -613,20 +639,18 @@ public class Contacts.EditorProperty : Gee.ArrayList<EditorPropertyRow> {
     if (details == null) {
       var parameters = new Gee.HashMultiMap<string, string> ();
       parameters["type"] = "HOME";
-      var address = new PostalAddress(null, null, null, null, null, null, null, null, null);
+      var address = new PostalAddress (null, null, null, null, null, null, null, null, null);
       var new_details = new PostalAddressFieldDetails (address, parameters);
-      details_set.add(new_details);
+      details_set.add (new_details);
       details = new_details;
     }
     var box = new EditorPropertyRow ("postal-addresses");
-    box.add_base_combo (details_set, _("Address"), TypeSet.general, details);
 
     var value_address = new AddressEditor (details);
-    box.container.pack_start (value_address);
-
+    box.set_main_widget (value_address);
     box.is_empty = value_address.is_empty ();
 
-    box.add_base_delete (details_set, details);
+    box.add_type_combo (details_set, TypeSet.general, details);
 
     value_address.changed.connect (() => {
       // Workaround: we shouldn't do a manual signal
diff --git a/src/contacts-esd-setup.vala b/src/contacts-esd-setup.vala
index b0c58adb..29a4530b 100644
--- a/src/contacts-esd-setup.vala
+++ b/src/contacts-esd-setup.vala
@@ -16,14 +16,10 @@
  *
  */
 
-// FIXME: the async bindings seem to be broken for this function.
-extern void e_trust_prompt_run_for_source (Gtk.Window parent, E.Source source, string certificate_pem, 
GLib.TlsCertificateFlags certificate_errors, string? error_text, bool allow_source_save, GLib.Cancellable? 
cancellable, AsyncReadyCallback callback);
-extern bool e_trust_prompt_run_for_source_finish (E.Source source, AsyncResult result, out 
E.TrustPromptResponse response) throws GLib.Error;
-
 namespace Contacts {
 
 public E.SourceRegistry? eds_source_registry = null;
-private E.CredentialsPrompter? eds_credentials_prompter = null;
+// private E.CredentialsPrompter? eds_credentials_prompter = null;
 
 public bool ensure_eds_accounts (bool allow_interaction) {
   if (eds_source_registry != null)
@@ -39,6 +35,8 @@ public bool ensure_eds_accounts (bool allow_interaction) {
     return false;
   }
 
+  // FIXME Do when GTK4 port of e-d-s-ui is done
+#if 0
   eds_credentials_prompter = new E.CredentialsPrompter (eds_source_registry);
 
   if (!allow_interaction)
@@ -64,28 +62,32 @@ public bool ensure_eds_accounts (bool allow_interaction) {
   // The eds_credentials_prompter responses to REQUIRED and REJECTED reasons,
   // the SSL_FAILED should be handled elsewhere.
   eds_source_registry.credentials_required.connect((src, reason, cert_pem, cert_err, err) => {
-    on_credentials_required.begin (src, reason, cert_pem, cert_err, err);
+      on_credentials_required.begin (src, reason, cert_pem, cert_err, err);
   });
 
   eds_credentials_prompter.process_awaiting_credentials ();
+#endif
 
   return true;
 }
 
+// FIXME Do when GTK4 port of e-d-s-ui is done
+#if 0
 private async void on_credentials_required (E.Source source, E.SourceCredentialsReason reason, string 
cert_pem, TlsCertificateFlags cert_errors, Error err) {
   if (eds_credentials_prompter.get_auto_prompt_disabled_for (source))
     return;
 
-  if (reason == E.SourceCredentialsReason.ERROR && err != null) {
-    warning ("Failed to authenticate for source \"%s\": %s", source.display_name, err.message);
-    return;
-  }
+   if (reason == E.SourceCredentialsReason.ERROR && err != null) {
+     warning ("Failed to authenticate for source \"%s\": %s",
+              source.display_name, err.message);
+     return;
+   }
 
-  if (reason == E.SourceCredentialsReason.SSL_FAILED) {
-    e_trust_prompt_run_for_source (eds_credentials_prompter.get_dialog_parent (),
+   if (reason == E.SourceCredentialsReason.SSL_FAILED) {
+     e_trust_prompt_run_for_source (eds_credentials_prompter.get_dialog_parent (),
         source, cert_pem, cert_errors, (err != null)? err.message : null, true,
         null, (obj, res) => on_source_trust_prompt_has_run.begin (source, res));
-  }
+   }
 }
 
 private async void on_source_trust_prompt_has_run (E.Source source, AsyncResult res) {
@@ -103,6 +105,7 @@ private async void on_source_trust_prompt_has_run (E.Source source, AsyncResult
     warning ("Failed to invoke authenticate() for source \"%s\": %s", source.display_name, e.message);
   }
 }
+#endif
 
 public bool has_goa_account () {
   foreach (var source in eds_source_registry.list_sources (E.SOURCE_EXTENSION_GOA)) {
@@ -154,7 +157,7 @@ public string? lookup_esource_name_by_uid (string uid) {
   return source.display_name;
 }
 
-public string? lookup_esource_name_by_uid_for_contact (string uid) {
+public unowned string? lookup_esource_name_by_uid_for_contact (string uid) {
   var source = eds_source_registry.ref_source (uid);
   if (source == null)
     return null;
@@ -188,6 +191,6 @@ public Gtk.Image? get_icon_for_goa_account (string goa_id) {
     return null;
   }
 
-  return new Gtk.Image.from_gicon (provider_icon, Gtk.IconSize.DIALOG);
+  return new Gtk.Image.from_gicon (provider_icon);
 }
 }
diff --git a/src/contacts-fake-persona-store.vala b/src/contacts-fake-persona-store.vala
index c6b8ae21..06437b4b 100644
--- a/src/contacts-fake-persona-store.vala
+++ b/src/contacts-fake-persona-store.vala
@@ -557,7 +557,7 @@ public class Contacts.FakeHashSet<T> : Gee.HashSet<T> {
   public override bool add (T element) {
     var res = base.add (element);
     if (res) {
-      added();
+      added ();
       changed ();
     }
     return res;
diff --git a/src/contacts-link-operation.vala b/src/contacts-link-operation.vala
new file mode 100644
index 00000000..9e75841d
--- /dev/null
+++ b/src/contacts-link-operation.vala
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2011 Alexander Larsson <alexl redhat com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+public class Contacts.LinkOperation : Object, Operation {
+
+  private weak Store store;
+
+  private Gee.LinkedList<Individual> individuals;
+  private Gee.HashSet<Gee.HashSet<Persona>> personas_to_link
+      = new Gee.HashSet<Gee.HashSet<Persona>> ();
+
+  private bool finished { get; set; default = false; }
+
+  private bool _reversable = false;
+  public bool reversable { get { return this._reversable; } }
+
+  private string _description;
+  public string description { owned get { return this._description; } }
+
+  public LinkOperation (Store store, Gee.LinkedList<Individual> individuals) {
+    this.store = store;
+    this.individuals = individuals;
+
+    this._description = ngettext ("Linked %d contact",
+                                  "Linked %d contacts", individuals.size)
+                        .printf (individuals.size);
+  }
+
+  /**
+   * Link individuals
+   */
+  public async void execute () throws GLib.Error {
+    var personas_to_link = new Gee.HashSet<Persona> ();
+    foreach (var i in individuals) {
+      var saved_personas = new Gee.HashSet<Persona> ();
+      foreach (var persona in i.personas) {
+        personas_to_link.add (persona);
+        saved_personas.add (persona);
+      }
+      this.personas_to_link.add (saved_personas);
+    }
+
+    yield this.store.aggregator.link_personas (personas_to_link);
+    this._reversable = true;
+    notify_property ("reversable");
+  }
+
+  /**
+   * Undoing means unlinking
+   */
+  public async void _undo () throws GLib.Error {
+    var individual = this.personas_to_link.first_match(() => {return true;})
+      .first_match(() => {return true;}).individual;
+
+    yield store.aggregator.unlink_individual (individual);
+
+    foreach (var personas in personas_to_link) {
+      yield this.store.aggregator.link_personas (personas);
+    }
+    this._reversable = false;
+    notify_property ("reversable");
+  }
+}
diff --git a/src/contacts-link-suggestion-grid.vala b/src/contacts-link-suggestion-grid.vala
index 967ed68f..bff1bec4 100644
--- a/src/contacts-link-suggestion-grid.vala
+++ b/src/contacts-link-suggestion-grid.vala
@@ -24,6 +24,7 @@ using Folks;
  */
 [GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-link-suggestion-grid.ui")]
 public class Contacts.LinkSuggestionGrid : Gtk.Grid {
+
   private const int AVATAR_SIZE = 54;
 
   [GtkChild]
@@ -39,13 +40,9 @@ public class Contacts.LinkSuggestionGrid : Gtk.Grid {
   public signal void suggestion_rejected ();
 
   public LinkSuggestionGrid (Individual individual) {
-    get_style_context ().add_class ("contacts-suggestion");
-
     var image_frame = new Avatar (AVATAR_SIZE, individual);
     image_frame.hexpand = false;
-    image_frame.margin = 12;
-    image_frame.show ();
-    attach (image_frame, 0, 0, 1, 2);
+    this.attach (image_frame, 0, 0, 1, 2);
 
     this.description_label.xalign = 0;
     this.description_label.label = Contacts.Utils.has_main_persona (individual) ?
@@ -56,7 +53,6 @@ public class Contacts.LinkSuggestionGrid : Gtk.Grid {
 
     var extra_info = find_extra_description (individual);
     if (extra_info != null) {
-      this.extra_info_label.show ();
       this.extra_info_label.label = extra_info;
     }
 
@@ -66,22 +62,22 @@ public class Contacts.LinkSuggestionGrid : Gtk.Grid {
 
   private string? find_extra_description (Individual individual) {
     // First try an email address
-    var emails = individual.email_addresses;
+    unowned var emails = individual.email_addresses;
     if (!emails.is_empty)
       return Utils.get_first<EmailFieldDetails> (emails).value;
 
     // Maybe a website? Works well with e.g. social media profiles
-    var urls = individual.urls;
+    unowned var urls = individual.urls;
     if (!urls.is_empty)
       return Utils.get_first<UrlFieldDetails> (urls).value;
 
     // Try a phone number
-    var phones = individual.phone_numbers;
+    unowned var phones = individual.phone_numbers;
     if (!phones.is_empty)
       return Utils.get_first<PhoneFieldDetails> (phones).value;
 
     // A postal address maybe?
-    var addresses = individual.postal_addresses;
+    unowned var addresses = individual.postal_addresses;
     if (!addresses.is_empty)
       return Utils.get_first<PostalAddressFieldDetails> (addresses).value.to_string ();
 
diff --git a/src/contacts-linked-personas-dialog.vala b/src/contacts-linked-personas-dialog.vala
index 26f3ca36..b66e94ce 100644
--- a/src/contacts-linked-personas-dialog.vala
+++ b/src/contacts-linked-personas-dialog.vala
@@ -40,18 +40,15 @@ public class Contacts.LinkedPersonasDialog : Gtk.Dialog {
 
     // loading personas for display
     var personas = Contacts.Utils.get_personas_for_display (individual);
-    bool is_first = true;
-    foreach (var p in personas) {
-      if (is_first) {
-        is_first = false;
-        continue;
-      }
-
+    for (int i = 1; i < personas.get_n_items (); i++) {
+      var p = (Persona) personas.get_item (i);
       var row_grid = new Gtk.Grid ();
 
       var image_frame = new Avatar (AVATAR_SIZE, individual);
       image_frame.set_hexpand (false);
-      image_frame.margin = 6;
+      image_frame.margin_top = 6;
+      image_frame.margin_bottom = 6;
+      image_frame.margin_start = 6;
       image_frame.margin_end = 12;
       row_grid.attach (image_frame, 0, 0, 1, 2);
 
@@ -73,7 +70,7 @@ public class Contacts.LinkedPersonasDialog : Gtk.Dialog {
       var button = new Gtk.Button.with_label (_("Unlink"));
       button.margin_end = 6;
       button.set_valign (Gtk.Align.CENTER);
-      button.get_child ().margin = 1;
+      // button.get_child ().margin = 1; XXX
       row_grid.attach (button, 2, 0, 1, 2);
 
       /* signal */
@@ -81,8 +78,7 @@ public class Contacts.LinkedPersonasDialog : Gtk.Dialog {
         // TODO: handly unlinking
         });
 
-      row_grid.show_all ();
-      this.linked_accounts_view.add (row_grid);
+      this.linked_accounts_view.append (row_grid);
     }
   }
 }
diff --git a/src/contacts-list-pane.vala b/src/contacts-list-pane.vala
index 5439b365..c30a8938 100644
--- a/src/contacts-list-pane.vala
+++ b/src/contacts-list-pane.vala
@@ -18,12 +18,12 @@
 using Folks;
 
 [GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-list-pane.ui")]
-public class Contacts.ListPane : Gtk.Frame {
+public class Contacts.ListPane : Adw.Bin {
   private Store store;
 
   [GtkChild]
-  private unowned Gtk.ScrolledWindow contacts_list_container;
-  private ContactList contacts_list;
+  private unowned Adw.Bin contacts_list_container;
+  private unowned ContactList contacts_list;
 
   [GtkChild]
   public unowned Gtk.SearchEntry filter_entry;
@@ -45,35 +45,33 @@ public class Contacts.ListPane : Gtk.Frame {
   public signal void delete_contacts (Gee.LinkedList<Individual> individual);
   public signal void contacts_marked (int contacts_marked);
 
-  public ListPane (Settings settings, Store contacts_store) {
+  public ListPane (Gtk.Window window, Settings settings, Store contacts_store) {
     this.store = contacts_store;
     this.notify["state"].connect (on_ui_state_changed);
 
+    this.filter_entry.set_key_capture_widget (window);
+
     // Build the filter query
     string[] filtered_fields = Query.MATCH_FIELDS_NAMES;
     foreach (var field in Query.MATCH_FIELDS_ADDRESSES)
       filtered_fields += field;
     this.filter_query = new SimpleQuery ("", filtered_fields);
 
-
     // Load the ContactsView and connect the necessary signals
-    this.contacts_list = new ContactList (settings, contacts_store, this.filter_query);
+    var contactslist = new ContactList (settings, contacts_store, this.filter_query);
+    this.contacts_list = contactslist;
+    this.contacts_list_container.set_child (contactslist);
     bind_property ("state", this.contacts_list, "state", BindingFlags.BIDIRECTIONAL | 
BindingFlags.SYNC_CREATE);
-    this.contacts_list_container.add (this.contacts_list);
 
-    this.contacts_list.selection_changed.connect( (l, individual) => {
+    this.contacts_list.selection_changed.connect ((l, individual) => {
         selection_changed (individual);
-      });
+    });
 
     this.contacts_list.contacts_marked.connect ((nr_contacts_marked) => {
         this.delete_button.sensitive = (nr_contacts_marked > 0);
         this.link_button.sensitive = (nr_contacts_marked > 1);
         contacts_marked (nr_contacts_marked);
-      });
-  }
-
-  public void undo_deletion () {
-    contacts_list.show_all ();
+    });
   }
 
   private void on_ui_state_changed (Object obj, ParamSpec pspec) {
@@ -82,7 +80,7 @@ public class Contacts.ListPane : Gtk.Frame {
         = this.contacts_list.sensitive
         = !this.state.editing ();
 
-    this.actions_bar.visible = (this.state == UiState.SELECTING);
+    this.actions_bar.revealed = (this.state == UiState.SELECTING);
   }
 
   [GtkCallback]
@@ -98,8 +96,8 @@ public class Contacts.ListPane : Gtk.Frame {
     this.contacts_list.scroll_to_contact ();
   }
 
-  public void hide_contact (Individual? individual) {
-    this.contacts_list.hide_contact (individual);
+  public void set_contact_visible (Individual? individual, bool visible) {
+    this.contacts_list.set_contact_visible (individual, visible);
   }
 
   [GtkCallback]
@@ -111,9 +109,4 @@ public class Contacts.ListPane : Gtk.Frame {
   private void on_delete_button_clicked (Gtk.Button delete_button) {
     delete_contacts (this.contacts_list.get_marked_contacts_and_hide ());
   }
-
-  /* Limiting width hack */
-  public override void get_preferred_width (out int minimum_width, out int natural_width) {
-    minimum_width = natural_width = 300;
-  }
 }
diff --git a/src/contacts-main-window.vala b/src/contacts-main-window.vala
index 47c67e47..4750f538 100644
--- a/src/contacts-main-window.vala
+++ b/src/contacts-main-window.vala
@@ -1,5 +1,6 @@
 /*
  * Copyright (C) 2011 Alexander Larsson <alexl redhat com>
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef gmail com>
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
@@ -18,49 +19,47 @@
 using Folks;
 
 [GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-main-window.ui")]
-public class Contacts.MainWindow : Hdy.ApplicationWindow {
-
-  private const GLib.ActionEntry[] action_entries = {
-    { "edit-contact",     edit_contact     },
-    { "share-contact",    share_contact    },
-    { "unlink-contact",   unlink_contact   },
-    { "delete-contact",   delete_contact   }
+public class Contacts.MainWindow : Adw.ApplicationWindow {
+
+  private const GLib.ActionEntry[] ACTION_ENTRIES = {
+    { "new-contact", new_contact },
+    { "edit-contact", edit_contact },
+    // { "share-contact", share_contact },
+    { "unlink-contact", unlink_contact },
+    { "delete-contact", delete_contact },
+    { "sort-on", null, "s", "'surname'", sort_on_changed },
+    { "undo-operation", undo_operation_action },
+    { "undo-delete", undo_delete_action },
   };
 
   [GtkChild]
-  private unowned Hdy.Leaflet header;
-  [GtkChild]
-  private unowned Hdy.Leaflet content_box;
+  private unowned Adw.Leaflet content_box;
   [GtkChild]
   private unowned Gtk.Revealer back_revealer;
   [GtkChild]
   private unowned Gtk.Stack list_pane_stack;
   [GtkChild]
-  private unowned Gtk.Container contact_pane_container;
+  private unowned Gtk.Overlay contact_pane_container;
+  [GtkChild]
+  private unowned Gtk.Box list_pane_page;
   [GtkChild]
-  private unowned Hdy.HeaderBar left_header;
+  private unowned Gtk.Box contact_pane_page;
   [GtkChild]
-  private unowned Gtk.Separator header_separator;
+  private unowned Adw.HeaderBar left_header;
   [GtkChild]
-  private unowned Hdy.HeaderBar right_header;
+  private unowned Adw.HeaderBar right_header;
   [GtkChild]
-  private unowned Gtk.Overlay notification_overlay;
+  private unowned Adw.ToastOverlay toast_overlay;
   [GtkChild]
   private unowned Gtk.Button select_cancel_button;
   [GtkChild]
   private unowned Gtk.MenuButton hamburger_menu_button;
   [GtkChild]
-  private unowned Gtk.ModelButton sort_on_firstname_button;
-  [GtkChild]
-  private unowned Gtk.ModelButton sort_on_surname_button;
-  [GtkChild]
-  private unowned Gtk.MenuButton contact_menu_button;
+  private unowned Gtk.Box contact_sheet_buttons;
   [GtkChild]
   private unowned Gtk.ToggleButton favorite_button;
   private bool ignore_favorite_button_toggled;
   [GtkChild]
-  private unowned Gtk.Button unlink_button;
-  [GtkChild]
   private unowned Gtk.Button add_button;
   [GtkChild]
   private unowned Gtk.Button cancel_button;
@@ -73,13 +72,15 @@ public class Contacts.MainWindow : Hdy.ApplicationWindow {
   private ListPane list_pane;
   private ContactPane contact_pane;
 
+  // Actions
+  private SimpleActionGroup actions = new SimpleActionGroup ();
+  private bool delete_cancelled;
+
   public UiState state { get; set; default = UiState.NORMAL; }
 
   // Window state
   public int window_width { get; set; }
   public int window_height { get; set; }
-  public bool window_maximized { get; set; }
-  public bool window_fullscreen { get; set; }
 
   public Settings settings { get; construct set; }
 
@@ -87,84 +88,41 @@ public class Contacts.MainWindow : Hdy.ApplicationWindow {
     get; construct set;
   }
 
-  construct {
-    SimpleActionGroup actions = new SimpleActionGroup ();
-    actions.add_action_entries (action_entries, this);
-    insert_action_group ("window", actions);
+  // If an unduable operation was recently performed, this will be set
+  public Operation? last_operation = null;
 
-    this.sort_on_firstname_button.clicked.connect (() => {
-      this.settings.sort_on_surname = false;
-      on_sort_changed ();
-    });
-    this.sort_on_surname_button.clicked.connect (() => {
-      this.settings.sort_on_surname = true;
-      on_sort_changed ();
-    });
-    on_sort_changed ();
+  construct {
+    this.actions.add_action_entries (ACTION_ENTRIES, this);
+    this.insert_action_group ("window", this.actions);
 
     this.notify["state"].connect (on_ui_state_changed);
 
-    create_contact_pane ();
-    connect_button_signals ();
-    restore_window_state ();
+    this.create_contact_pane ();
+    this.connect_button_signals ();
+    this.restore_window_state ();
 
     if (Config.PROFILE == "development")
-        get_style_context ().add_class ("devel");
+        this.add_css_class ("devel");
   }
 
   public MainWindow (Settings settings, App app, Store contacts_store) {
     Object (
       application: app,
       settings: settings,
-      show_menubar: false,
-      visible: true,
       store: contacts_store
     );
-  }
 
-  private void on_sort_changed () {
-    this.sort_on_firstname_button.active = !this.settings.sort_on_surname;
-    this.sort_on_surname_button.active = this.settings.sort_on_surname;
+    unowned var sort_key = this.settings.sort_on_surname? "surname" : "firstname";
+    var sort_action = (SimpleAction) this.actions.lookup_action ("sort-on");
+    sort_action.set_state (new Variant.string (sort_key));
   }
 
   private void restore_window_state () {
-    // Load initial values
-    this.window_width = this.settings.window_width;
-    this.window_height = this.settings.window_height;
-    this.window_maximized = this.settings.window_maximized;
-    this.window_fullscreen = this.settings.window_fullscreen;
-
     // Apply them
-    if (this.window_width > 0 && this.window_height > 0)
-      set_default_size (this.window_width, this.window_height);
-    if (this.window_maximized)
-      maximize ();
-    if (this.window_fullscreen)
-      fullscreen ();
-  }
-
-  public override bool window_state_event (Gdk.EventWindowState event) {
-    this.window_maximized = (Gdk.WindowState.MAXIMIZED in event.new_window_state);
-    this.window_fullscreen = (Gdk.WindowState.FULLSCREEN in event.new_window_state);
-
-    return base.window_state_event (event);
-  }
-
-  // Called on window resize. Save window size for the next start.
-  public override void size_allocate (Gtk.Allocation allocation) {
-    base.size_allocate (allocation);
-
-    if (this.window_fullscreen || this.window_maximized)
-      return;
-
-    // Get the size via widget.get_size() instead of the allocation
-    // so that the window isn't ever-expanding (in case of CSD).
-    int width = 0;
-    int height = 0;
-    get_size(out width, out height);
-
-    this.window_width = width;
-    this.window_height = height;
+    if (this.settings.window_width > 0 && this.settings.window_height > 0)
+      set_default_size (this.settings.window_width, this.settings.window_height);
+    this.maximized = this.settings.window_maximized;
+    this.fullscreened = this.settings.window_fullscreen;
   }
 
   private void create_contact_pane () {
@@ -172,10 +130,7 @@ public class Contacts.MainWindow : Hdy.ApplicationWindow {
     this.contact_pane.visible = true;
     this.contact_pane.hexpand = true;
     this.contact_pane.contacts_linked.connect (contact_pane_contacts_linked_cb);
-    this.contact_pane.display_name_changed.connect ((display_name) => {
-      this.right_header.title = display_name;
-    });
-    this.contact_pane_container.add (this.contact_pane);
+    this.contact_pane_container.set_child (this.contact_pane);
   }
 
   /**
@@ -186,30 +141,28 @@ public class Contacts.MainWindow : Hdy.ApplicationWindow {
   public void show_contact_list () {
     // FIXME: if no contact is loaded per backend, I must place a sign
     // saying "import your contacts/add online account"
-    if (list_pane != null)
+    if (this.list_pane != null)
       return;
 
-    list_pane = new ListPane (this.settings, store);
+    this.list_pane = new ListPane (this, this.settings, store);
     bind_property ("state", this.list_pane, "state", BindingFlags.BIDIRECTIONAL | BindingFlags.SYNC_CREATE);
-    list_pane.selection_changed.connect (list_pane_selection_changed_cb);
-    list_pane.link_contacts.connect (list_pane_link_contacts_cb);
-    list_pane.delete_contacts.connect (delete_contacts);
-
-    list_pane.contacts_marked.connect ((nr_contacts) => {
-        if (this.state == UiState.SELECTING)
-          this.left_header.title = ngettext ("%d Selected", "%d Selected", nr_contacts)
-                                       .printf (nr_contacts);
-        else
-          this.left_header.title = _("Contacts");
-      });
-
-    list_pane_stack.add (list_pane);
-    list_pane.show ();
-    list_pane_stack.visible_child = list_pane;
+    this.list_pane.selection_changed.connect (list_pane_selection_changed_cb);
+    this.list_pane.link_contacts.connect (list_pane_link_contacts_cb);
+    this.list_pane.delete_contacts.connect (delete_contacts);
+
+    this.list_pane.contacts_marked.connect ((nr_contacts) => {
+      string left_title = _("Contacts");
+      if (this.state == UiState.SELECTING)
+        left_title = ngettext ("%d Selected", "%d Selected", nr_contacts)
+                                     .printf (nr_contacts);
+      this.left_header.title_widget = new Adw.WindowTitle (left_title, "");
+    });
 
-    if (this.contact_pane.individual != null)
-      list_pane.select_contact (this.contact_pane.individual);
+    this.list_pane_stack.add_child (this.list_pane);
+    this.list_pane_stack.visible_child = this.list_pane;
 
+    if (this.contact_pane.individual != null)
+      this.list_pane.select_contact (this.contact_pane.individual);
   }
 
   private void on_ui_state_changed (Object obj, ParamSpec pspec) {
@@ -219,8 +172,7 @@ public class Contacts.MainWindow : Hdy.ApplicationWindow {
         = (this.state == UiState.NORMAL || this.state == UiState.SHOWING);
 
     // UI when showing a contact
-    this.contact_menu_button.visible
-      = this.favorite_button.visible
+    this.contact_sheet_buttons.visible
       = (this.state == UiState.SHOWING);
 
     // Selecting UI
@@ -228,41 +180,25 @@ public class Contacts.MainWindow : Hdy.ApplicationWindow {
     this.selection_button.visible = !(this.state == UiState.SELECTING || this.state.editing ());
 
     if (this.state != UiState.SELECTING)
-      this.left_header.title = _("Contacts");
+      this.left_header.title_widget = new Adw.WindowTitle (_("Contacts"), "");
 
     // Editing UI
     this.cancel_button.visible
         = this.done_button.visible
+        = this.right_header.show_end_title_buttons
         = this.state.editing ();
+    this.right_header.show_end_title_buttons = !this.state.editing ();
     if (this.state.editing ()) {
-      this.done_button.use_underline = true;
       this.done_button.label = (this.state == UiState.CREATING)? _("_Add") : _("Done");
       // Cast is required because Gtk.Button.set_focus_on_click is deprecated and
       // we have to use Gtk.Widget.set_focus_on_click instead
-      ((Gtk.Widget) this.done_button).set_focus_on_click (true);
+      this.done_button.set_focus_on_click (true);
     }
-    // When selecting or editing, we get special headerbars
-    set_selection_mode (this.state == UiState.SELECTING || this.state.editing ());
 
     // Allow the back gesture when not browsing
-    this.content_box.can_swipe_back = this.state == UiState.NORMAL ||
-                                      this.state == UiState.SHOWING ||
-                                      this.state == UiState.SELECTING;
-  }
-
-  private void set_selection_mode (bool selection_mode) {
-    unowned var left_ctx = this.left_header.get_style_context ();
-    unowned var separator_ctx = this.header_separator.get_style_context ();
-    unowned var right_ctx = this.right_header.get_style_context ();
-    if (selection_mode) {
-      left_ctx.add_class ("selection-mode");
-      separator_ctx.add_class ("selection-mode");
-      right_ctx.add_class ("selection-mode");
-    } else {
-      left_ctx.remove_class ("selection-mode");
-      separator_ctx.remove_class ("selection-mode");
-      right_ctx.remove_class ("selection-mode");
-    }
+    this.content_box.can_navigate_back = this.state == UiState.NORMAL ||
+                                         this.state == UiState.SHOWING ||
+                                         this.state == UiState.SELECTING;
   }
 
   [GtkCallback]
@@ -270,18 +206,15 @@ public class Contacts.MainWindow : Hdy.ApplicationWindow {
     show_list_pane ();
   }
 
-  private void share_contact () {
-    debug ("Share isn't implemented, yet");
-  }
-
-  private void edit_contact () {
+  private void edit_contact (GLib.SimpleAction action, GLib.Variant? parameter) {
     if (this.contact_pane.individual == null)
       return;
 
     this.state = UiState.UPDATING;
 
     unowned var name = this.contact_pane.individual.display_name;
-    this.right_header.title = _("Editing %s").printf (name);
+    var title = _("Editing %s").printf (name);
+    this.right_header.title_widget = new Adw.WindowTitle (title, "");
     this.contact_pane.edit_contact ();
   }
 
@@ -300,43 +233,70 @@ public class Contacts.MainWindow : Hdy.ApplicationWindow {
   [GtkCallback]
   private void on_selection_button_clicked () {
     this.state = UiState.SELECTING;
-    this.left_header.title = ngettext ("%d Selected", "%d Selected", 0)
-                                       .printf (0);
+    var left_title = ngettext ("%d Selected", "%d Selected", 0) .printf (0);
+    this.left_header.title_widget = new Adw.WindowTitle (left_title, "");
   }
 
-  private void unlink_contact () {
-    var individual = this.contact_pane.individual;
+  private void unlink_contact (GLib.SimpleAction action, GLib.Variant? parameter) {
+    unowned var individual = this.contact_pane.individual;
     if (individual == null)
       return;
 
     set_shown_contact (null);
     this.state = UiState.NORMAL;
 
-    var operation = new UnLinkOperation (this.store);
-    operation.execute.begin (individual);
-
-    var b = new Gtk.Button.with_mnemonic (_("_Undo"));
-    var notification = new InAppNotification (_("Contacts unlinked"), b);
+    this.last_operation = new UnlinkOperation (this.store, individual);
+    this.last_operation.execute.begin ((obj, res) => {
+      try {
+        this.last_operation.execute.end (res);
+      } catch (GLib.Error e) {
+        warning ("Error unlinking individuals: %s", e.message);
+      }
+    });
 
-    /* signal handlers */
-    b.clicked.connect ( () => {
-        /* here, we will link the thing in question */
-        operation.undo.begin ();
-        notification.dismiss ();
-      });
+    var toast = new Adw.Toast (this.last_operation.description);
+    toast.set_button_label (_("_Undo"));
+    toast.action_name = "window.undo-operation";
 
-    add_notification (notification);
+    this.toast_overlay.add_toast (toast);
   }
 
-  private void delete_contact () {
+  private void delete_contact (GLib.SimpleAction action, GLib.Variant? parameter) {
     var individual = this.contact_pane.individual;
     if (individual == null)
       return;
 
-    this.list_pane.hide_contact (individual);
+    this.list_pane.set_contact_visible (individual, false);
     delete_contacts (new Gee.ArrayList<Individual>.wrap ({ individual }));
   }
 
+  private void sort_on_changed (SimpleAction action, GLib.Variant? new_state) {
+    unowned var sort_key = new_state.get_string ();
+    this.settings.sort_on_surname = (sort_key == "surname");
+    action.set_state (new_state);
+  }
+
+  private void undo_operation_action (SimpleAction action, GLib.Variant? parameter) {
+    if (this.last_operation == null) {
+      warning ("Undo action was called without anything that can be undone?");
+      return;
+    }
+
+    debug ("Undoing operation '%s'", this.last_operation.description);
+    this.last_operation.undo.begin ((obj, res) => {
+      try {
+        this.last_operation.undo.end (res);
+      } catch (GLib.Error e) {
+        warning ("Couldn't undo operation '%s': %s", this.last_operation.description, e.message);
+      }
+      debug ("Finished undoing operation '%s'", this.last_operation.description);
+    });
+  }
+
+  private void undo_delete_action (SimpleAction action, GLib.Variant? parameter) {
+    this.delete_cancelled = true;
+  }
+
   private void stop_editing (bool cancel = false) {
     if (this.state == UiState.CREATING) {
       if (cancel) {
@@ -350,16 +310,7 @@ public class Contacts.MainWindow : Hdy.ApplicationWindow {
     this.contact_pane.stop_editing (cancel);
     this.list_pane.scroll_to_contact ();
 
-    if (this.contact_pane.individual != null) {
-      this.right_header.title = this.contact_pane.individual.display_name;
-    } else {
-      this.right_header.title = "";
-    }
-  }
-
-  public void add_notification (InAppNotification notification) {
-    this.notification_overlay.add_overlay (notification);
-    notification.show ();
+    this.right_header.title_widget = new Adw.WindowTitle ("", "");
   }
 
   public void set_shown_contact (Individual? i) {
@@ -372,20 +323,17 @@ public class Contacts.MainWindow : Hdy.ApplicationWindow {
       list_pane.select_contact (i);
 
     // clearing right_header
+    this.right_header.title_widget = new Adw.WindowTitle ("", "");
     if (i != null) {
       this.ignore_favorite_button_toggled = true;
       this.favorite_button.active = i.is_favourite;
       this.ignore_favorite_button_toggled = false;
       this.favorite_button.tooltip_text = (i.is_favourite)? _("Unmark as favorite")
-                                                                     : _("Mark as favorite");
-      this.right_header.title = i.display_name;
-    } else {
-      this.right_header.title = "";
+                                                          : _("Mark as favorite");
     }
   }
 
-  [GtkCallback]
-  public void new_contact () {
+  public void new_contact (GLib.SimpleAction action, GLib.Variant? parameter) {
     if (this.state == UiState.UPDATING || this.state == UiState.CREATING)
       return;
 
@@ -393,7 +341,7 @@ public class Contacts.MainWindow : Hdy.ApplicationWindow {
 
     this.state = UiState.CREATING;
 
-    this.right_header.title = _("New Contact");
+    this.right_header.title_widget = new Adw.WindowTitle (_("New Contact"), "");
 
     this.contact_pane.new_contact ();
     show_contact_pane ();
@@ -411,7 +359,8 @@ public class Contacts.MainWindow : Hdy.ApplicationWindow {
 
   [GtkCallback]
   private void on_child_transition_running () {
-    if (!content_box.child_transition_running && content_box.visible_child_name == "list-pane")
+    if (!this.content_box.child_transition_running &&
+         this.content_box.visible_child == this.list_pane_page)
       this.list_pane.select_contact (null);
   }
 
@@ -420,21 +369,21 @@ public class Contacts.MainWindow : Hdy.ApplicationWindow {
       this.back_revealer.visible =
         this.content_box.folded &&
         !this.cancel_button.visible &&
-        this.header.visible_child == this.right_header;
+        this.content_box.visible_child == this.contact_pane_page;
   }
 
   private void show_list_pane () {
-    content_box.visible_child_name = "list-pane";
+    this.content_box.navigate (Adw.NavigationDirection.BACK);
     update_header ();
   }
 
   private void show_contact_pane () {
-    content_box.visible_child_name = "contact-pane";
+    this.content_box.navigate (Adw.NavigationDirection.FORWARD);
     update_header ();
   }
 
   public void show_search (string query) {
-    list_pane.filter_entry.set_text (query);
+    this.list_pane.filter_entry.set_text (query);
   }
 
   private void connect_button_signals () {
@@ -452,49 +401,25 @@ public class Contacts.MainWindow : Hdy.ApplicationWindow {
       unowned var individual = this.contact_pane.individual;
       if (individual == null)
         return;
-      this.unlink_button.set_visible (individual.personas.size > 1);
-    });
-  }
 
-  [GtkCallback]
-  bool key_press_event_cb (Gdk.EventKey event) {
-    if ((event.keyval == Gdk.keyval_from_name ("q")) &&
-        ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0)) {
-      // Clear the contacts so any changed information is stored
-      this.contact_pane.show_contact (null);
-      destroy ();
-    } else if (((event.keyval == Gdk.Key.s) ||
-                (event.keyval == Gdk.Key.f)) &&
-               ((event.state & Gdk.ModifierType.CONTROL_MASK) != 0)) {
-      // Explicitly check if this.list_pane is already initialized,
-      // or we might crash at startup
-      if (this.list_pane != null && this.list_pane.filter_entry != null)
-          Utils.grab_entry_focus_no_select (this.list_pane.filter_entry);
-    } else if (event.length >= 1 &&
-               Gdk.keyval_to_unicode (event.keyval) != 0 &&
-               (event.state & Gdk.ModifierType.CONTROL_MASK) == 0 &&
-               (event.state & Gdk.ModifierType.MOD1_MASK) == 0 &&
-               (event.keyval != Gdk.Key.Escape) &&
-               (event.keyval != Gdk.Key.Tab) &&
-               (event.keyval != Gdk.Key.BackSpace) ) {
-      // Explicitly check if this.list_pane is already initialized,
-      // or we might crash at startup
-      if (this.list_pane != null && this.list_pane.filter_entry != null)
-          Utils.grab_entry_focus_no_select (this.list_pane.filter_entry);
-      propagate_key_event (event);
-    }
-
-    return false;
+      var unlink_action = this.actions.lookup_action ("unlink-contact");
+      ((SimpleAction) unlink_action).set_enabled (individual.personas.size > 1);
+    });
   }
 
-  [GtkCallback]
-  bool delete_event_cb (Gdk.EventAny event) {
+  public override bool close_request () {
     // Clear the contacts so any changed information is stored
     this.contact_pane.show_contact (null);
-    return false;
+
+    this.settings.window_width = this.default_width;
+    this.settings.window_height = this.default_height;
+    this.settings.window_maximized = this.maximized;
+    this.settings.window_fullscreen = this.fullscreened;
+
+    return base.close_request ();
   }
 
-  void list_pane_selection_changed_cb (Individual? new_selection) {
+  private void list_pane_selection_changed_cb (Individual? new_selection) {
     set_shown_contact (new_selection);
     if (this.state != UiState.SELECTING)
       this.state = UiState.SHOWING;
@@ -503,105 +428,59 @@ public class Contacts.MainWindow : Hdy.ApplicationWindow {
       show_contact_pane ();
   }
 
-  void list_pane_link_contacts_cb (Gee.LinkedList<Individual> contact_list) {
+  private void list_pane_link_contacts_cb (Gee.LinkedList<Individual> contact_list) {
     set_shown_contact (null);
     this.state = UiState.NORMAL;
 
-    var operation = new LinkOperation (this.store);
-    operation.execute.begin (contact_list);
-
-    string msg = ngettext ("%d contacts linked",
-                           "%d contacts linked",
-                           contact_list.size).printf (contact_list.size);
-
-    var b = new Gtk.Button.with_mnemonic (_("_Undo"));
-    var notification = new InAppNotification (msg, b);
-
-    /* signal handlers */
-    b.clicked.connect ( () => {
-        /* here, we will unlink the thing in question */
-        operation.undo.begin ();
-        notification.dismiss ();
-      });
+    this.last_operation = new LinkOperation (this.store, contact_list);
+    this.last_operation.execute.begin ((obj, res) => {
+      try {
+        this.last_operation.execute.end (res);
+      } catch (GLib.Error e) {
+        warning ("Error linking individuals: %s", e.message);
+      }
+    });
 
-    add_notification (notification);
+    var toast = new Adw.Toast (this.last_operation.description);
+    toast.set_button_label (_("_Undo"));
+    toast.action_name = "window.undo-operation";
+    this.toast_overlay.add_toast (toast);
   }
 
   private void delete_contacts (Gee.List<Individual> individuals) {
     set_shown_contact (null);
     this.state = UiState.NORMAL;
 
-    string msg;
-    if (individuals.size == 1)
-      msg = _("Deleted contact %s").printf (individuals[0].display_name);
-    else
-      msg = ngettext ("%d contact deleted", "%d contacts deleted", individuals.size)
-              .printf (individuals.size);
-
-    var b = new Gtk.Button.with_mnemonic (_("_Undo"));
-
-    var notification = new InAppNotification (msg, b);
-
-    // Don't wrap (default), but ellipsize
-    notification.message_label.wrap = false;
-    notification.message_label.max_width_chars = 45;
-    notification.message_label.ellipsize = Pango.EllipsizeMode.END;
-
-    // signal handlers
-    bool really_delete = true;
-    b.clicked.connect ( () => {
-        really_delete = false;
-        notification.dismiss ();
-
-        /* Reset the contact list */
-        list_pane.undo_deletion ();
-
-        set_shown_contact (individuals[0]);
-        this.state = UiState.SHOWING;
-      });
-    notification.dismissed.connect ( () => {
-        if (really_delete)
-          foreach (var i in individuals)
-            foreach (var p in i.personas) {
-              // TODO: make sure it is acctally removed
-              p.store.remove_persona.begin (p, (obj, res) => {
-                try {
-                  p.store.remove_persona.end (res);
-                } catch (Error e) {
-                  debug ("Coudln't remove persona: %s", e.message);
-                }
-              });
-            }
-      });
-
-    add_notification (notification);
-  }
-
-  void contact_pane_contacts_linked_cb (string? main_contact, string linked_contact, LinkOperation 
operation) {
-    string msg;
-    if (main_contact != null)
-      msg = _("%s linked to %s").printf (main_contact, linked_contact);
-    else
-      msg = _("%s linked to the contact").printf (linked_contact);
-
-    var b = new Gtk.Button.with_mnemonic (_("_Undo"));
-    var notification = new InAppNotification (msg, b);
-
-    b.clicked.connect ( () => {
-        notification.dismiss ();
-        operation.undo.begin ();
-      });
-
-    add_notification (notification);
-  }
-
-  // Override the default destroy() to save the window state
-  public override void destroy () {
-    this.settings.window_width = this.window_width;
-    this.settings.window_height = this.window_height;
-    this.settings.window_maximized = this.window_maximized;
-    this.settings.window_fullscreen = this.window_fullscreen;
-
-    base.destroy ();
+    this.last_operation = new DeleteOperation (individuals);
+    var toast = new Adw.Toast (this.last_operation.description);
+    toast.set_button_label (_("_Undo"));
+    toast.action_name = "window.undo-delete";
+
+    this.delete_cancelled = false;
+    toast.dismissed.connect (() => {
+        if (this.delete_cancelled) {
+          this.list_pane.set_contact_visible (individuals[0], true);
+          set_shown_contact (individuals[0]);
+          this.state = UiState.SHOWING;
+        } else {
+          this.last_operation.execute.begin ((obj, res) => {
+              try {
+                this.last_operation.execute.end (res);
+              } catch (Error e) {
+                debug ("Coudln't remove persona: %s", e.message);
+              }
+          });
+        }
+    });
+
+    this.toast_overlay.add_toast (toast);
+  }
+
+  private void contact_pane_contacts_linked_cb (string? main_contact, string linked_contact, LinkOperation 
operation) {
+    this.last_operation = operation;
+    var toast = new Adw.Toast (this.last_operation.description);
+    toast.set_button_label (_("_Undo"));
+    toast.action_name = "window.undo-operation";
+    this.toast_overlay.add_toast (toast);
   }
 }
diff --git a/src/contacts-operation.vala b/src/contacts-operation.vala
new file mode 100644
index 00000000..f77944b4
--- /dev/null
+++ b/src/contacts-operation.vala
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2021 Niels De Graef <nielsdegraef redhat com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * Contacts.Operation is a simple interface to describe actions that can be
+ * executed and possibly undone later on (for example, using a button on an
+ * in-app notification).
+ *
+ * Since some operations might not be able undoable later onwards, there is a
+ * property `reversable` that you should check first before calling undo().
+ */
+public interface Contacts.Operation : Object {
+
+  /**
+   * Whether undo() can be called on this object
+   */
+  public abstract bool reversable { get; }
+
+  /**
+   * A user-facing string that tells us what the operation does
+   */
+  public abstract string description { owned get; }
+
+  /**
+   * This the actual implementation of the operation that a subclass needs to
+   * implement.
+   */
+  public abstract async void execute () throws GLib.Error;
+
+  /**
+   * The is the public API undo. If you want, you can override it still, e.g.
+   * to provide better warnings.
+   */
+  public virtual async void undo () throws GLib.Error {
+    // FIXME: should throw an error instead so we can show something to the user
+    if (!this.reversable) {
+      warning ("Can't undo '%s'", this.description);
+      return;
+    }
+
+    yield this._undo ();
+  }
+
+  /**
+   * This the actual implementation of the undo that a subclass needs to
+   * implement.
+   */
+  protected abstract async void _undo () throws GLib.Error;
+}
diff --git a/src/contacts-setup-window.vala b/src/contacts-setup-window.vala
index 9305b8a9..157d1d6e 100644
--- a/src/contacts-setup-window.vala
+++ b/src/contacts-setup-window.vala
@@ -18,9 +18,10 @@
 using Folks;
 
 [GtkTemplate (ui = "/org/gnome/Contacts/ui/contacts-setup-window.ui")]
-public class Contacts.SetupWindow : Hdy.ApplicationWindow {
+public class Contacts.SetupWindow : Adw.ApplicationWindow {
+
   [GtkChild]
-  private unowned Gtk.Box content;
+  private unowned Adw.Clamp clamp;
 
   [GtkChild]
   private unowned Gtk.Button setup_done_button;
@@ -36,8 +37,7 @@ public class Contacts.SetupWindow : Hdy.ApplicationWindow {
     Object (application: app, icon_name: Config.APP_ID);
     this.setup_accounts_list = new AccountsList (store);
     this.setup_accounts_list.hexpand = true;
-    this.setup_accounts_list.show ();
-    this.content.add (this.setup_accounts_list);
+    this.clamp.set_child (this.setup_accounts_list);
 
     // Listen for changes
     store.backend_store.backend_available.connect  ( () => {
@@ -46,14 +46,14 @@ public class Contacts.SetupWindow : Hdy.ApplicationWindow {
 
     ulong id2 = 0;
     id2 = this.setup_accounts_list.account_selected.connect (() => {
-        this.setup_done_button.set_sensitive (true);
+        this.setup_done_button.sensitive = true;
         this.setup_accounts_list.disconnect (id2);
       });
 
     fill_accounts_list (store);
 
     this.setup_done_button.clicked.connect (() => {
-        var selected_store = this.setup_accounts_list.selected_store as Edsf.PersonaStore;
+        unowned var selected_store = this.setup_accounts_list.selected_store as Edsf.PersonaStore;
         setup_done (selected_store);
       });
 
diff --git a/src/contacts-type-combo.vala b/src/contacts-type-combo.vala
index 7104fcf9..3f252f7d 100644
--- a/src/contacts-type-combo.vala
+++ b/src/contacts-type-combo.vala
@@ -18,77 +18,47 @@
 using Folks;
 
 /**
- * The TypeCombo is a widget that fills itself with the types of a certain
+ * The TypeComboRow is a widget that fills itself with the types of a certain
  * category (using {@link Contacts.TypeSet}). For example, it allows the user
  * to choose between "Personal", "Home" and "Work" for email addresses,
  * together with all the custom labels it has encountered since then.
  */
-public class Contacts.TypeCombo : Gtk.ComboBox  {
+public class Contacts.TypeComboRow : Adw.ComboRow  {
 
-  private unowned TypeSet type_set;
-
-  /**
-   * The {@link Contacts.TypeDescriptor} that is currently shown
-   */
-  public TypeDescriptor active_descriptor {
-    get {
-      Gtk.TreeIter iter;
-
-      get_active_iter (out iter);
-      assert (!is_separator (this.model, iter));
-
-      unowned TypeDescriptor descriptor;
-      this.model.get (iter, 1, out descriptor);
-      return descriptor;
-    }
-    set {
-      set_active_iter (value.iter);
-    }
+  public TypeDescriptor selected_descriptor {
+    get { return (TypeDescriptor) this.selected_item; }
   }
 
-  construct {
-    this.valign = Gtk.Align.START;
-    this.halign = Gtk.Align.FILL;
-    this.hexpand = true;
-    this.visible = true;
-
-    var renderer = new Gtk.CellRendererText ();
-    pack_start (renderer, true);
-    set_attributes (renderer, "text", 0);
-
-    set_row_separator_func (is_separator);
+  public TypeSet type_set {
+    get { return (TypeSet) this.model; }
   }
 
   /**
-   * Creates a TypeCombo for the given TypeSet. To set the active value,
-   * use the "current-decsriptor" property, set_active_from_field_details(),
-   * or set_active_from_vcard_type()
+   * Creates a TypeComboRow for the given TypeSet.
    */
-  public TypeCombo (TypeSet type_set) {
-    this.type_set = type_set;
-    this.model = type_set.store;
-  }
-
-  private bool is_separator (Gtk.TreeModel model, Gtk.TreeIter iter) {
-    unowned string? s;
-    model.get (iter, 0, out s);
-    return s == null;
+  public TypeComboRow (TypeSet type_set) {
+    Object (
+      model: type_set,
+      expression: new Gtk.PropertyExpression (typeof (TypeDescriptor), null, "display-name")
+    );
   }
 
   /**
    * Sets the value to the type of the given {@link Folks.AbstractFieldDetails}.
    */
-  public void set_active_from_field_details (AbstractFieldDetails details) {
-    this.active_descriptor = this.type_set.lookup_descriptor_for_field_details (details);
+  public void set_selected_from_field_details (AbstractFieldDetails details) {
+    uint position = 0;
+    this.type_set.lookup_by_field_details (details, out position);
+    this.selected = position;
   }
 
   /**
    * Sets the value to the type that best matches the given vcard type
    * (for example "HOME" or "WORK").
    */
-  public void set_active_from_vcard_type (string type) {
-    Gtk.TreeIter iter;
-    this.type_set.get_iter_for_vcard_type (type, out iter);
-    set_active_iter (iter);
+  public void set_selected_from_vcard_type (string type) {
+    uint position = 0;
+    this.type_set.lookup_by_vcard_type (type, out position);
+    this.selected = position;
   }
 }
diff --git a/src/contacts-typeset.vala b/src/contacts-typeset.vala
index 3208ac47..0149b685 100644
--- a/src/contacts-typeset.vala
+++ b/src/contacts-typeset.vala
@@ -22,7 +22,7 @@ using Folks;
  * phone number can be both for a personal phone, a work phone or even a fax
  * machine.
  */
-public class Contacts.TypeSet : Object  {
+public class Contacts.TypeSet : Object, GLib.ListModel  {
 
   /** Returns the category of typeset (mostly used for debugging). */
   public string category { get; construct set; }
@@ -31,61 +31,23 @@ public class Contacts.TypeSet : Object  {
   private TypeDescriptor other_dummy = new TypeDescriptor.other ();
 
   // List of VcardTypeMapping. This makes sure of keeping the correct order
-  private Gee.List<VcardTypeMapping?> vcard_type_mappings
-      = new Gee.ArrayList<VcardTypeMapping?> ();
+  private GenericArray<VcardTypeMapping?> vcard_type_mappings
+      = new GenericArray<VcardTypeMapping?> ();
 
-  // Contains 2 columns:
-  // 1. The type's display name (or null for a separator)
-  // 2. The TypeDescriptor
-  public Gtk.ListStore store { get; private set; }
+  private GenericArray<TypeDescriptor> descriptors = new GenericArray<TypeDescriptor> ();
 
   /**
    * Creates a TypeSet for the given category, e.g. "phones" (used for debugging)
    */
   private TypeSet (string? category) {
     Object (category: category);
-
-    this.store = new Gtk.ListStore (2, typeof (unowned string?), typeof (TypeDescriptor));
-  }
-
-  /**
-   * Returns the {@link Gtk.TreeIter} which corresponds to the type of the
-   * given {@link Folks.AbstractFieldDetails}.
-   */
-  public void get_iter_for_field_details (AbstractFieldDetails detail,
-                                          out Gtk.TreeIter iter) {
-    // Note that we shouldn't have null here, but it's there just to be sure.
-    var d = lookup_descriptor_for_field_details (detail);
-    iter = d.iter;
-  }
-
-  /**
-   * Returns the {@link Gtk.TreeIter} which corresponds the best to the given
-   * vcard type.
-   *
-   * @param type A VCard-like type, such as "HOME" or "CELL".
-   */
-  public void get_iter_for_vcard_type (string type, out Gtk.TreeIter iter) {
-    unowned TypeDescriptor? d = lookup_descriptor_by_vcard_type (type);
-    iter = (d != null)? d.iter : this.other_dummy.iter;
-  }
-
-  /**
-   * Returns the {@link Gtk.TreeIter} which corresponds the best to the given
-   * custom label.
-   */
-  public void get_iter_for_custom_label (string label, out Gtk.TreeIter iter) {
-    var descr = get_descriptor_for_custom_label (label);
-    if (descr == null)
-      descr = create_descriptor_for_custom_label (label);
-    iter = descr.iter;
   }
 
   /**
    * Returns the display name for the type of the given AbstractFieldDetails.
    */
   public unowned string format_type (AbstractFieldDetails detail) {
-    var d = lookup_descriptor_for_field_details (detail);
+    var d = lookup_by_field_details (detail);
     return d.display_name;
   }
 
@@ -93,15 +55,10 @@ public class Contacts.TypeSet : Object  {
    * Adds the TypeDescriptor to the {@link TypeSet}'s store.
    * @param descriptor The TypeDescription to be added
    */
-  private void add_descriptor_to_store (TypeDescriptor descriptor) {
+  private void add_descriptor (TypeDescriptor descriptor) {
     debug ("%s: Adding type %s to store", this.category, descriptor.to_string ());
-
-    if (descriptor.is_custom ())
-      this.store.insert_before (out descriptor.iter, null);
-    else
-      this.store.append (out descriptor.iter);
-
-    store.set (descriptor.iter, 0, descriptor.display_name, 1, descriptor);
+    this.descriptors.add (descriptor);
+    this.items_changed (this.descriptors.length - 1, 0, 1);
   }
 
   /**
@@ -111,60 +68,57 @@ public class Contacts.TypeSet : Object  {
    * @param display_name The translated display name
    * @return The appropriate TypeDescriptor or null if no match was found.
    */
-  public unowned TypeDescriptor? lookup_descriptor_in_store (string display_name) {
-    Gtk.TreeIter iter;
-
-    // Make sure we handle an empty store
-    if (!this.store.get_iter_first (out iter))
-      return null;
-
-    do {
-      unowned TypeDescriptor? type_descr;
-      this.store.get (iter, 1, out type_descr);
-
-      if (display_name.ascii_casecmp (type_descr.display_name) == 0)
-        return type_descr;
-      if (display_name.ascii_casecmp (type_descr.name) == 0)
-        return type_descr;
-    } while (this.store.iter_next (ref iter));
+  public unowned TypeDescriptor? lookup_by_display_name (string display_name,
+                                                         out uint position) {
+    for (int i = 0; i < this.descriptors.length; i++) {
+      unowned var type_descr = this.descriptors[i];
+
+      if (display_name.ascii_casecmp (type_descr.display_name) != 0)
+        continue;
+      if (display_name.ascii_casecmp (type_descr.name) != 0)
+        continue;
+
+      position = i;
+      return type_descr;
+    }
 
     // Nothing was found
+    position = 0;
     return null;
   }
 
   private void add_vcard_mapping (VcardTypeMapping vcard_mapping) {
-    TypeDescriptor? descriptor = lookup_descriptor_in_store (vcard_mapping.name);
+    uint position;
+    var descriptor = lookup_by_display_name (vcard_mapping.name, out position);
     if (descriptor == null) {
       descriptor = new TypeDescriptor.vcard (vcard_mapping.name, vcard_mapping.types);
-      add_descriptor_to_store (descriptor);
+      debug ("%s: Adding VCard type %s to store", this.category, descriptor.to_string ());
+      this.add_descriptor (descriptor);
     }
 
     this.vcard_type_mappings.add (vcard_mapping);
   }
 
-  // Refers to the type of the detail, i.e. "Other" instead of "Personal" or "Work"
-  private void add_type_other () {
-    store.append (out other_dummy.iter);
-    store.set (other_dummy.iter, 0, other_dummy.display_name, 1, other_dummy);
-  }
-
   /**
    * Tries to find the TypeDescriptor matching the given custom label, or null if none.
    */
-  public unowned TypeDescriptor? get_descriptor_for_custom_label (string label) {
+  public TypeDescriptor? lookup_by_custom_label (string label,
+                                                 out uint position) {
     // Check in the current display names
-    unowned TypeDescriptor? descriptor = lookup_descriptor_in_store (label);
+    unowned var descriptor = lookup_by_display_name (label, out position);
     if (descriptor != null)
       return descriptor;
 
     // Try again, but use the vcard types too
-    descriptor = lookup_descriptor_by_vcard_type (label);
+    descriptor = lookup_by_vcard_type (label, out position);
     return descriptor;
   }
 
   private TypeDescriptor create_descriptor_for_custom_label (string label) {
     var new_descriptor = new TypeDescriptor.custom (label);
-    add_descriptor_to_store (new_descriptor);
+    debug ("%s: Adding custom type %s to store",
+           this.category, new_descriptor.to_string ());
+    this.add_descriptor (new_descriptor);
     return new_descriptor;
   }
 
@@ -172,19 +126,26 @@ public class Contacts.TypeSet : Object  {
    * Returns the TypeDescriptor which corresponds the best to the given vcard type.
    * @param str A VCard-like type, such as "HOME" or "CELL".
    */
-  private unowned TypeDescriptor? lookup_descriptor_by_vcard_type (string str) {
-    foreach (VcardTypeMapping? mapping in this.vcard_type_mappings) {
+  public unowned TypeDescriptor? lookup_by_vcard_type (string str,
+                                                       out uint position) {
+    foreach (unowned var mapping in this.vcard_type_mappings) {
       if (mapping.contains (str))
-        return lookup_descriptor_in_store (mapping.name);
+        return lookup_by_display_name (mapping.name, out position);
     }
 
+    position = 0;
     return null;
   }
 
-  public TypeDescriptor lookup_descriptor_for_field_details (AbstractFieldDetails detail) {
+  /**
+   * Looks up the TypeDescriptor for the given field details. If the descriptor
+   * is not found, it will be created and returned, so this never returns null.
+   */
+  public TypeDescriptor lookup_by_field_details (AbstractFieldDetails detail,
+                                                 out uint position = null) {
     if (detail.parameters.contains (TypeDescriptor.X_GOOGLE_LABEL)) {
       var label = Utils.get_first<string> (detail.parameters[TypeDescriptor.X_GOOGLE_LABEL]);
-      var descriptor = get_descriptor_for_custom_label (label);
+      var descriptor = lookup_by_custom_label (label, out position);
       // Still didn't find it => create it
       if (descriptor == null)
         descriptor = create_descriptor_for_custom_label (label);
@@ -197,14 +158,28 @@ public class Contacts.TypeSet : Object  {
       return this.other_dummy;
     }
 
-    foreach (VcardTypeMapping? d in this.vcard_type_mappings) {
-      if (d.matches (types))
-        return lookup_descriptor_in_store (d.name);
+    foreach (unowned var mapping in this.vcard_type_mappings) {
+      if (mapping.matches (types))
+        return lookup_by_display_name (mapping.name, out position);
     }
 
     return this.other_dummy;
   }
 
+  public GLib.Type get_item_type () {
+    return typeof (TypeDescriptor);
+  }
+
+  public uint get_n_items () {
+    return this.descriptors.length;
+  }
+
+  public GLib.Object? get_item (uint i) {
+    if (i > this.descriptors.length)
+      return null;
+
+    return this.descriptors[i];
+  }
 
   private static TypeSet _general;
   private const VcardTypeMapping[] general_data = {
@@ -218,7 +193,8 @@ public class Contacts.TypeSet : Object  {
         _general = new TypeSet ("General");
         for (int i = 0; i < general_data.length; i++)
           _general.add_vcard_mapping (general_data[i]);
-        _general.add_type_other ();
+
+        _general.add_descriptor (general.other_dummy);
       }
 
       return _general;
@@ -238,7 +214,7 @@ public class Contacts.TypeSet : Object  {
         _email = new TypeSet ("Emails");
         for (int i = 0; i < email_data.length; i++)
           _email.add_vcard_mapping (email_data[i]);
-        _email.add_type_other ();
+        _email.add_descriptor (_email.other_dummy);
       }
 
       return _email;
@@ -275,7 +251,7 @@ public class Contacts.TypeSet : Object  {
         _phone = new TypeSet ("Phones");
         for (int i = 0; i < phone_data.length; i++)
           _phone.add_vcard_mapping (phone_data[i]);
-        _phone.add_type_other ();
+        _phone.add_descriptor (_phone.other_dummy);
       }
 
       return _phone;
diff --git a/src/contacts-unlink-operation.vala b/src/contacts-unlink-operation.vala
new file mode 100644
index 00000000..7b67679b
--- /dev/null
+++ b/src/contacts-unlink-operation.vala
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 Alexander Larsson <alexl redhat com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+using Folks;
+
+public class Contacts.UnlinkOperation : Object, Operation {
+
+  private weak Store store;
+
+  private Individual individual;
+
+  private Gee.HashSet<Persona> personas = new Gee.HashSet<Persona> ();
+
+  private bool _reversable = false;
+  public bool reversable { get { return this._reversable; } }
+
+  private string _description;
+  public string description { owned get { return this._description; } }
+
+  public UnlinkOperation (Store store, Individual main) {
+    this.store = store;
+    this.individual = main;
+    this._description = _("Unlinking contacts");
+  }
+
+  /* Remove a personas from individual */
+  public async void execute () throws GLib.Error {
+    foreach (var persona in this.individual.personas)
+      this.personas.add (persona);
+
+    yield store.aggregator.unlink_individual (this.individual);
+    this._reversable = true;
+    notify_property ("reversable");
+  }
+
+  /* Undo the unlinking */
+  public async void _undo () throws GLib.Error {
+    yield this.store.aggregator.link_personas (personas);
+    this._reversable = false;
+    notify_property ("reversable");
+  }
+}
diff --git a/src/contacts-utils.vala b/src/contacts-utils.vala
index 48388765..667c6911 100644
--- a/src/contacts-utils.vala
+++ b/src/contacts-utils.vala
@@ -65,13 +65,10 @@ namespace Contacts {
 }
 
 namespace Contacts.Utils {
+
   public void compose_mail (string email) {
     var mailto_uri = "mailto:"; + Uri.escape_string (email, "@" , false);
-    try {
-      Gtk.show_uri_on_window (null, mailto_uri, 0);
-    } catch (Error e) {
-      debug ("Couldn't launch URI \"%s\": %s", mailto_uri, e.message);
-    }
+    Gtk.show_uri (null, mailto_uri, 0);
   }
 
 #if HAVE_TELEPATHY
@@ -116,7 +113,7 @@ namespace Contacts.Utils {
     return null;
   }
 
-  public void grab_entry_focus_no_select (Gtk.Entry entry) {
+  public void grab_entry_focus_no_select (Gtk.SearchEntry entry) {
     int start, end;
     if (!entry.get_selection_bounds (out start, out end)) {
       start = end = entry.get_position ();
@@ -189,8 +186,8 @@ namespace Contacts.Utils {
                                         Gtk.MessageType.ERROR,
                                         Gtk.ButtonsType.OK,
                                         "%s", error);
-    dialog.run();
-    dialog.destroy();
+    dialog.response.connect(() => { dialog.destroy(); });
+    dialog.show();
   }
 
   public bool persona_is_main (Persona persona) {
@@ -348,18 +345,23 @@ namespace Contacts.Utils {
     return false;
   }
 
-  public Gee.List<Persona> get_personas_for_display (Individual individual) {
-    CompareDataFunc<Persona> compare_persona_by_store = (a, b) => {
-      unowned var store_a = a.store;
-      unowned var store_b = b.store;
+  public ListModel get_personas_for_display (Individual individual) {
+    var persona_list = new ListStore(typeof(Persona));
+    foreach (var persona in individual.personas)
+      if (persona.store.type_id != "key-file")
+        persona_list.append (persona);
+
+    persona_list.sort ((a, b) => {
+      unowned var store_a = ((Persona) a).store;
+      unowned var store_b = ((Persona) b).store;
 
       // In the same store, sort Google 'other' contacts last
       if (store_a == store_b) {
-        if (!persona_is_google (a))
+        if (!persona_is_google ((Persona) a))
           return 0;
 
-        var a_is_other = persona_is_google_other (a);
-        if (a_is_other != persona_is_google_other (b))
+        var a_is_other = persona_is_google_other ((Persona) a);
+        if (a_is_other != persona_is_google_other ((Persona) b))
           return a_is_other? 1 : -1;
       }
 
@@ -373,14 +375,8 @@ namespace Contacts.Utils {
 
       // Normal case: use alphabetical sorting
       return strcmp (store_a.id, store_b.id);
-    };
-
-    var persona_list = new Gee.ArrayList<Persona>();
-    foreach (var persona in individual.personas)
-      if (persona.store.type_id != "key-file")
-        persona_list.add (persona);
+    });
 
-    persona_list.sort ((owned) compare_persona_by_store);
     return persona_list;
   }
 
@@ -567,6 +563,50 @@ namespace Contacts.Utils {
     }
   }
 
+  // A helper struct to keep track on general properties on how each Persona
+  // property should be displayed
+  private struct PropertyDisplayInfo {
+    string property_name;
+    string display_name;
+    string icon_name;
+  }
+
+  private const PropertyDisplayInfo[] display_infos = {
+    { "alias", N_("Alias"), null },
+    { "avatar", N_("Avatar"), "emblem-photos-symbolic" },
+    { "birthday", N_("Birthday"), "birthday-symbolic" },
+    { "calendar-event-id", N_("Calendar event"), "x-office-calendar-symbolic" },
+    { "email-addresses", N_("Email address"), "mail-unread-symbolic" },
+    { "full-name", N_("Full name"), null },
+    { "gender", N_("Gender"), null },
+    { "groups", N_("Group"), null },
+    { "im-addresses", N_("Instant messaging"), "user-available-symbolic" },
+    { "is-favourite", N_("Favourite"), "emblem-favorite-symbolic" },
+    { "local-ids", N_("Local ID"), null },
+    { "nickname", N_("Nickname"), "avatar-default-symbolic" },
+    { "notes", N_("Note"), "note-symbolic" },
+    { "phone-numbers", N_("Phone number"), "phone-symbolic" },
+    { "postal-addresses", N_("Address"), "mark-location-symbolic" },
+    { "roles", N_("Role"), null },
+    { "structured-name", N_("Structured name"), "avatar-default-symbolic" },
+    { "urls", N_("Website"), "web-browser-symbolic" },
+    { "web-service-addresses", N_("Web service"), null },
+  };
+
+  public unowned string get_display_name_for_property (string property_name) {
+    foreach (unowned var info in display_infos)
+      if (info.property_name == property_name)
+        return gettext (info.display_name);
+    return_val_if_reached (null);
+  }
+
+  public unowned string? get_icon_name_for_property (string property_name) {
+    foreach (unowned var info in display_infos)
+      if (info.property_name == property_name)
+        return info.icon_name;
+    return null;
+  }
+
 #if HAVE_TELEPATHY
   public void fetch_contact_info (Individual individual) {
     /* TODO: Ideally Folks should have API for this (#675131) */
diff --git a/src/main.vala b/src/main.vala
index 284a0d43..346c601c 100644
--- a/src/main.vala
+++ b/src/main.vala
@@ -23,6 +23,8 @@ main (string[] args) {
   Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8");
   Intl.textdomain (Config.GETTEXT_PACKAGE);
 
+  GLib.Environment.set_application_name (_("Contacts"));
+
 #if HAVE_CHEESE
   GtkCheese.init (ref args);
 #endif
diff --git a/src/meson.build b/src/meson.build
index d9776384..fc11d335 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -6,12 +6,16 @@ install_data('org.gnome.Contacts.gschema.xml',
 
 # Common library
 libcontacts_sources = files(
+  'contacts-delete-operation.vala',
   'contacts-esd-setup.vala',
   'contacts-fake-persona-store.vala',
   'contacts-im-service.vala',
+  'contacts-link-operation.vala',
+  'contacts-operation.vala',
   'contacts-store.vala',
   'contacts-typeset.vala',
   'contacts-type-descriptor.vala',
+  'contacts-unlink-operation.vala',
   'contacts-utils.vala',
   'contacts-vcard-type-mapping.vala',
 )
@@ -24,7 +28,6 @@ contacts_vala_args = [
 
 contacts_c_args = [
   '-include', 'config.h',
-  '-DGNOME_DESKTOP_USE_UNSTABLE_API',
   '-DLOCALEDIR="@0@"'.format(locale_dir),
 ]
 
@@ -34,22 +37,16 @@ contacts_deps = [
   gee,
   gio_unix,
   glib,
-  gnome_desktop,
   goa,
-  gtk,
+  gtk4_dep,
+  libadwaita_dep,
   libebook,
   libedataserver,
-  libedataserverui,
-  libhandy,
+  # libedataserverui,
+  libportal_dep,
   math,
 ]
 
-# Add extra stuff given a certain set of options
-if cheese_dep.found() and cheese_gtk_dep.found()
-  contacts_deps += [ cheese_dep, cheese_gtk_dep ]
-  contacts_vala_args += [ '-D', 'HAVE_CHEESE' ]
-endif
-
 if get_option('telepathy')
   contacts_deps += [ folks_telepathy, telepathy_glib ]
   contacts_vala_args += [ '-D', 'HAVE_TELEPATHY' ]
@@ -82,13 +79,11 @@ contacts_vala_sources = files(
   'contacts-contact-list.vala',
   'contacts-contact-pane.vala',
   'contacts-contact-sheet.vala',
-  'contacts-crop-cheese-dialog.vala',
+  'contacts-crop-dialog.vala',
   'contacts-editor-persona.vala',
   'contacts-editor-property.vala',
-  'contacts-in-app-notification.vala',
   'contacts-link-suggestion-grid.vala',
   'contacts-linked-personas-dialog.vala',
-  'contacts-linking.vala',
   'contacts-list-pane.vala',
   'contacts-main-window.vala',
   'contacts-settings.vala',
@@ -102,9 +97,9 @@ contacts_c_sources = [
   'cc-crop-area.c',
 ]
 
-if cheese_dep.found() and cheese_gtk_dep.found()
-  contacts_c_sources += 'cheese-flash.c'
-endif
+# if cheese_dep.found() and cheese_gtk_dep.found()
+#   contacts_c_sources += 'cheese-flash.c'
+# endif
 
 contacts_sources = [
   contacts_c_sources,
diff --git a/vapi/custom.vapi b/vapi/custom.vapi
index c48e3e8c..5d1484d8 100644
--- a/vapi/custom.vapi
+++ b/vapi/custom.vapi
@@ -1,11 +1,11 @@
 [CCode (cprefix = "Cc", lower_case_cprefix = "cc_", cheader_filename = "cc-crop-area.h")]
 namespace Cc {
-       public class CropArea : Gtk.DrawingArea {
-               [CCode (has_construct_function = false, type = "GtkWidget*")]
-               public CropArea ();
-               public void set_min_size (int width, int height);
-               public void set_constrain_aspect (bool  constrain);
-               public void set_picture (Gdk.Pixbuf pixbuf);
-               public Gdk.Pixbuf get_picture ();
-       }
+    public class CropArea : Gtk.Widget {
+        [CCode (has_construct_function = false, type = "GtkWidget*")]
+        public CropArea ();
+        public void set_min_size (int width, int height);
+        public void set_paintable (Gdk.Paintable paintable);
+        public Gdk.Paintable get_paintable ();
+        public Gdk.Pixbuf create_pixbuf ();
+    }
 }


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]