mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-08 07:35:01 +03:00
Merge branch 'main' into contact-panel-keyboard-nav
This commit is contained in:
commit
f54d74eda9
Before Width: | Height: | Size: 184 B After Width: | Height: | Size: 184 B |
@ -341,6 +341,19 @@
|
||||
"icon_color": "#efecf4",
|
||||
"background": "#5852605c"
|
||||
}
|
||||
},
|
||||
"badge": {
|
||||
"corner_radius": 3,
|
||||
"padding": 2,
|
||||
"margin": {
|
||||
"bottom": -1,
|
||||
"right": -1
|
||||
},
|
||||
"border": {
|
||||
"width": 1,
|
||||
"color": "#26232a"
|
||||
},
|
||||
"background": "#576ddb"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -470,6 +483,33 @@
|
||||
"color": "#efecf4",
|
||||
"size": 14,
|
||||
"background": "#000000aa"
|
||||
},
|
||||
"notification": {
|
||||
"margin": {
|
||||
"top": 10
|
||||
},
|
||||
"background": "#26232a",
|
||||
"corner_radius": 6,
|
||||
"padding": 12,
|
||||
"border": {
|
||||
"color": "#19171c",
|
||||
"width": 1
|
||||
},
|
||||
"shadow": {
|
||||
"blur": 16,
|
||||
"color": "#0000003d",
|
||||
"offset": [
|
||||
0,
|
||||
2
|
||||
]
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"width": 380,
|
||||
"margin": {
|
||||
"right": 10,
|
||||
"bottom": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
@ -1639,5 +1679,56 @@
|
||||
"padding": {
|
||||
"left": 6
|
||||
}
|
||||
},
|
||||
"contact_notification": {
|
||||
"header_avatar": {
|
||||
"height": 12,
|
||||
"width": 12,
|
||||
"corner_radius": 6
|
||||
},
|
||||
"header_message": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#e2dfe7",
|
||||
"size": 12,
|
||||
"margin": {
|
||||
"left": 8,
|
||||
"right": 8
|
||||
}
|
||||
},
|
||||
"header_height": 18,
|
||||
"body_message": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#8b8792",
|
||||
"size": 12,
|
||||
"margin": {
|
||||
"left": 20,
|
||||
"top": 6,
|
||||
"bottom": 6
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#e2dfe7",
|
||||
"size": 12,
|
||||
"background": "#19171c",
|
||||
"padding": 4,
|
||||
"corner_radius": 6,
|
||||
"margin": {
|
||||
"left": 6
|
||||
},
|
||||
"hover": {
|
||||
"background": "#26232a3d"
|
||||
}
|
||||
},
|
||||
"dismiss_button": {
|
||||
"color": "#8b8792",
|
||||
"icon_width": 8,
|
||||
"icon_height": 8,
|
||||
"button_width": 8,
|
||||
"button_height": 8,
|
||||
"hover": {
|
||||
"color": "#e2dfe7"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -341,6 +341,19 @@
|
||||
"icon_color": "#19171c",
|
||||
"background": "#8b87922e"
|
||||
}
|
||||
},
|
||||
"badge": {
|
||||
"corner_radius": 3,
|
||||
"padding": 2,
|
||||
"margin": {
|
||||
"bottom": -1,
|
||||
"right": -1
|
||||
},
|
||||
"border": {
|
||||
"width": 1,
|
||||
"color": "#e2dfe7"
|
||||
},
|
||||
"background": "#576ddb"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -470,6 +483,33 @@
|
||||
"color": "#19171c",
|
||||
"size": 14,
|
||||
"background": "#000000aa"
|
||||
},
|
||||
"notification": {
|
||||
"margin": {
|
||||
"top": 10
|
||||
},
|
||||
"background": "#e2dfe7",
|
||||
"corner_radius": 6,
|
||||
"padding": 12,
|
||||
"border": {
|
||||
"color": "#efecf4",
|
||||
"width": 1
|
||||
},
|
||||
"shadow": {
|
||||
"blur": 16,
|
||||
"color": "#0000001f",
|
||||
"offset": [
|
||||
0,
|
||||
2
|
||||
]
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"width": 380,
|
||||
"margin": {
|
||||
"right": 10,
|
||||
"bottom": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
@ -1639,5 +1679,56 @@
|
||||
"padding": {
|
||||
"left": 6
|
||||
}
|
||||
},
|
||||
"contact_notification": {
|
||||
"header_avatar": {
|
||||
"height": 12,
|
||||
"width": 12,
|
||||
"corner_radius": 6
|
||||
},
|
||||
"header_message": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#26232a",
|
||||
"size": 12,
|
||||
"margin": {
|
||||
"left": 8,
|
||||
"right": 8
|
||||
}
|
||||
},
|
||||
"header_height": 18,
|
||||
"body_message": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#585260",
|
||||
"size": 12,
|
||||
"margin": {
|
||||
"left": 20,
|
||||
"top": 6,
|
||||
"bottom": 6
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#26232a",
|
||||
"size": 12,
|
||||
"background": "#efecf4",
|
||||
"padding": 4,
|
||||
"corner_radius": 6,
|
||||
"margin": {
|
||||
"left": 6
|
||||
},
|
||||
"hover": {
|
||||
"background": "#e2dfe71f"
|
||||
}
|
||||
},
|
||||
"dismiss_button": {
|
||||
"color": "#585260",
|
||||
"icon_width": 8,
|
||||
"icon_height": 8,
|
||||
"button_width": 8,
|
||||
"button_height": 8,
|
||||
"hover": {
|
||||
"color": "#26232a"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -341,6 +341,19 @@
|
||||
"icon_color": "#ffffff",
|
||||
"background": "#2b2b2b"
|
||||
}
|
||||
},
|
||||
"badge": {
|
||||
"corner_radius": 3,
|
||||
"padding": 2,
|
||||
"margin": {
|
||||
"bottom": -1,
|
||||
"right": -1
|
||||
},
|
||||
"border": {
|
||||
"width": 1,
|
||||
"color": "#1c1c1c"
|
||||
},
|
||||
"background": "#2472f2"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -470,6 +483,33 @@
|
||||
"color": "#ffffff",
|
||||
"size": 14,
|
||||
"background": "#000000aa"
|
||||
},
|
||||
"notification": {
|
||||
"margin": {
|
||||
"top": 10
|
||||
},
|
||||
"background": "#1c1c1c",
|
||||
"corner_radius": 6,
|
||||
"padding": 12,
|
||||
"border": {
|
||||
"color": "#070707",
|
||||
"width": 1
|
||||
},
|
||||
"shadow": {
|
||||
"blur": 16,
|
||||
"color": "#00000052",
|
||||
"offset": [
|
||||
0,
|
||||
2
|
||||
]
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"width": 380,
|
||||
"margin": {
|
||||
"right": 10,
|
||||
"bottom": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
@ -1639,5 +1679,56 @@
|
||||
"padding": {
|
||||
"left": 6
|
||||
}
|
||||
},
|
||||
"contact_notification": {
|
||||
"header_avatar": {
|
||||
"height": 12,
|
||||
"width": 12,
|
||||
"corner_radius": 6
|
||||
},
|
||||
"header_message": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#f1f1f1",
|
||||
"size": 12,
|
||||
"margin": {
|
||||
"left": 8,
|
||||
"right": 8
|
||||
}
|
||||
},
|
||||
"header_height": 18,
|
||||
"body_message": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#9c9c9c",
|
||||
"size": 12,
|
||||
"margin": {
|
||||
"left": 20,
|
||||
"top": 6,
|
||||
"bottom": 6
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#f1f1f1",
|
||||
"size": 12,
|
||||
"background": "#0e0e0e80",
|
||||
"padding": 4,
|
||||
"corner_radius": 6,
|
||||
"margin": {
|
||||
"left": 6
|
||||
},
|
||||
"hover": {
|
||||
"background": "#070707"
|
||||
}
|
||||
},
|
||||
"dismiss_button": {
|
||||
"color": "#9c9c9c",
|
||||
"icon_width": 8,
|
||||
"icon_height": 8,
|
||||
"button_width": 8,
|
||||
"button_height": 8,
|
||||
"hover": {
|
||||
"color": "#c6c6c6"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -341,6 +341,19 @@
|
||||
"icon_color": "#000000",
|
||||
"background": "#e3e3e3"
|
||||
}
|
||||
},
|
||||
"badge": {
|
||||
"corner_radius": 3,
|
||||
"padding": 2,
|
||||
"margin": {
|
||||
"bottom": -1,
|
||||
"right": -1
|
||||
},
|
||||
"border": {
|
||||
"width": 1,
|
||||
"color": "#f8f8f8"
|
||||
},
|
||||
"background": "#484bed"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -470,6 +483,33 @@
|
||||
"color": "#000000",
|
||||
"size": 14,
|
||||
"background": "#000000aa"
|
||||
},
|
||||
"notification": {
|
||||
"margin": {
|
||||
"top": 10
|
||||
},
|
||||
"background": "#f8f8f8",
|
||||
"corner_radius": 6,
|
||||
"padding": 12,
|
||||
"border": {
|
||||
"color": "#d5d5d5",
|
||||
"width": 1
|
||||
},
|
||||
"shadow": {
|
||||
"blur": 16,
|
||||
"color": "#0000001f",
|
||||
"offset": [
|
||||
0,
|
||||
2
|
||||
]
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"width": 380,
|
||||
"margin": {
|
||||
"right": 10,
|
||||
"bottom": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
@ -1639,5 +1679,56 @@
|
||||
"padding": {
|
||||
"left": 6
|
||||
}
|
||||
},
|
||||
"contact_notification": {
|
||||
"header_avatar": {
|
||||
"height": 12,
|
||||
"width": 12,
|
||||
"corner_radius": 6
|
||||
},
|
||||
"header_message": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#2b2b2b",
|
||||
"size": 12,
|
||||
"margin": {
|
||||
"left": 8,
|
||||
"right": 8
|
||||
}
|
||||
},
|
||||
"header_height": 18,
|
||||
"body_message": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#474747",
|
||||
"size": 12,
|
||||
"margin": {
|
||||
"left": 20,
|
||||
"top": 6,
|
||||
"bottom": 6
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#2b2b2b",
|
||||
"size": 12,
|
||||
"background": "#f1f1f1",
|
||||
"padding": 4,
|
||||
"corner_radius": 6,
|
||||
"margin": {
|
||||
"left": 6
|
||||
},
|
||||
"hover": {
|
||||
"background": "#e3e3e3"
|
||||
}
|
||||
},
|
||||
"dismiss_button": {
|
||||
"color": "#717171",
|
||||
"icon_width": 8,
|
||||
"icon_height": 8,
|
||||
"button_width": 8,
|
||||
"button_height": 8,
|
||||
"hover": {
|
||||
"color": "#393939"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -341,6 +341,19 @@
|
||||
"icon_color": "#fdf6e3",
|
||||
"background": "#586e755c"
|
||||
}
|
||||
},
|
||||
"badge": {
|
||||
"corner_radius": 3,
|
||||
"padding": 2,
|
||||
"margin": {
|
||||
"bottom": -1,
|
||||
"right": -1
|
||||
},
|
||||
"border": {
|
||||
"width": 1,
|
||||
"color": "#073642"
|
||||
},
|
||||
"background": "#268bd2"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -470,6 +483,33 @@
|
||||
"color": "#fdf6e3",
|
||||
"size": 14,
|
||||
"background": "#000000aa"
|
||||
},
|
||||
"notification": {
|
||||
"margin": {
|
||||
"top": 10
|
||||
},
|
||||
"background": "#073642",
|
||||
"corner_radius": 6,
|
||||
"padding": 12,
|
||||
"border": {
|
||||
"color": "#002b36",
|
||||
"width": 1
|
||||
},
|
||||
"shadow": {
|
||||
"blur": 16,
|
||||
"color": "#0000003d",
|
||||
"offset": [
|
||||
0,
|
||||
2
|
||||
]
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"width": 380,
|
||||
"margin": {
|
||||
"right": 10,
|
||||
"bottom": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
@ -1639,5 +1679,56 @@
|
||||
"padding": {
|
||||
"left": 6
|
||||
}
|
||||
},
|
||||
"contact_notification": {
|
||||
"header_avatar": {
|
||||
"height": 12,
|
||||
"width": 12,
|
||||
"corner_radius": 6
|
||||
},
|
||||
"header_message": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#eee8d5",
|
||||
"size": 12,
|
||||
"margin": {
|
||||
"left": 8,
|
||||
"right": 8
|
||||
}
|
||||
},
|
||||
"header_height": 18,
|
||||
"body_message": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#93a1a1",
|
||||
"size": 12,
|
||||
"margin": {
|
||||
"left": 20,
|
||||
"top": 6,
|
||||
"bottom": 6
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#eee8d5",
|
||||
"size": 12,
|
||||
"background": "#002b36",
|
||||
"padding": 4,
|
||||
"corner_radius": 6,
|
||||
"margin": {
|
||||
"left": 6
|
||||
},
|
||||
"hover": {
|
||||
"background": "#0736423d"
|
||||
}
|
||||
},
|
||||
"dismiss_button": {
|
||||
"color": "#93a1a1",
|
||||
"icon_width": 8,
|
||||
"icon_height": 8,
|
||||
"button_width": 8,
|
||||
"button_height": 8,
|
||||
"hover": {
|
||||
"color": "#eee8d5"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -341,6 +341,19 @@
|
||||
"icon_color": "#002b36",
|
||||
"background": "#93a1a12e"
|
||||
}
|
||||
},
|
||||
"badge": {
|
||||
"corner_radius": 3,
|
||||
"padding": 2,
|
||||
"margin": {
|
||||
"bottom": -1,
|
||||
"right": -1
|
||||
},
|
||||
"border": {
|
||||
"width": 1,
|
||||
"color": "#eee8d5"
|
||||
},
|
||||
"background": "#268bd2"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -470,6 +483,33 @@
|
||||
"color": "#002b36",
|
||||
"size": 14,
|
||||
"background": "#000000aa"
|
||||
},
|
||||
"notification": {
|
||||
"margin": {
|
||||
"top": 10
|
||||
},
|
||||
"background": "#eee8d5",
|
||||
"corner_radius": 6,
|
||||
"padding": 12,
|
||||
"border": {
|
||||
"color": "#fdf6e3",
|
||||
"width": 1
|
||||
},
|
||||
"shadow": {
|
||||
"blur": 16,
|
||||
"color": "#0000001f",
|
||||
"offset": [
|
||||
0,
|
||||
2
|
||||
]
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"width": 380,
|
||||
"margin": {
|
||||
"right": 10,
|
||||
"bottom": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
@ -1639,5 +1679,56 @@
|
||||
"padding": {
|
||||
"left": 6
|
||||
}
|
||||
},
|
||||
"contact_notification": {
|
||||
"header_avatar": {
|
||||
"height": 12,
|
||||
"width": 12,
|
||||
"corner_radius": 6
|
||||
},
|
||||
"header_message": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#073642",
|
||||
"size": 12,
|
||||
"margin": {
|
||||
"left": 8,
|
||||
"right": 8
|
||||
}
|
||||
},
|
||||
"header_height": 18,
|
||||
"body_message": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#586e75",
|
||||
"size": 12,
|
||||
"margin": {
|
||||
"left": 20,
|
||||
"top": 6,
|
||||
"bottom": 6
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#073642",
|
||||
"size": 12,
|
||||
"background": "#fdf6e3",
|
||||
"padding": 4,
|
||||
"corner_radius": 6,
|
||||
"margin": {
|
||||
"left": 6
|
||||
},
|
||||
"hover": {
|
||||
"background": "#eee8d51f"
|
||||
}
|
||||
},
|
||||
"dismiss_button": {
|
||||
"color": "#586e75",
|
||||
"icon_width": 8,
|
||||
"icon_height": 8,
|
||||
"button_width": 8,
|
||||
"button_height": 8,
|
||||
"hover": {
|
||||
"color": "#073642"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -341,6 +341,19 @@
|
||||
"icon_color": "#f5f7ff",
|
||||
"background": "#5e66875c"
|
||||
}
|
||||
},
|
||||
"badge": {
|
||||
"corner_radius": 3,
|
||||
"padding": 2,
|
||||
"margin": {
|
||||
"bottom": -1,
|
||||
"right": -1
|
||||
},
|
||||
"border": {
|
||||
"width": 1,
|
||||
"color": "#293256"
|
||||
},
|
||||
"background": "#3d8fd1"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -470,6 +483,33 @@
|
||||
"color": "#f5f7ff",
|
||||
"size": 14,
|
||||
"background": "#000000aa"
|
||||
},
|
||||
"notification": {
|
||||
"margin": {
|
||||
"top": 10
|
||||
},
|
||||
"background": "#293256",
|
||||
"corner_radius": 6,
|
||||
"padding": 12,
|
||||
"border": {
|
||||
"color": "#202746",
|
||||
"width": 1
|
||||
},
|
||||
"shadow": {
|
||||
"blur": 16,
|
||||
"color": "#0000003d",
|
||||
"offset": [
|
||||
0,
|
||||
2
|
||||
]
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"width": 380,
|
||||
"margin": {
|
||||
"right": 10,
|
||||
"bottom": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
@ -1639,5 +1679,56 @@
|
||||
"padding": {
|
||||
"left": 6
|
||||
}
|
||||
},
|
||||
"contact_notification": {
|
||||
"header_avatar": {
|
||||
"height": 12,
|
||||
"width": 12,
|
||||
"corner_radius": 6
|
||||
},
|
||||
"header_message": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#dfe2f1",
|
||||
"size": 12,
|
||||
"margin": {
|
||||
"left": 8,
|
||||
"right": 8
|
||||
}
|
||||
},
|
||||
"header_height": 18,
|
||||
"body_message": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#979db4",
|
||||
"size": 12,
|
||||
"margin": {
|
||||
"left": 20,
|
||||
"top": 6,
|
||||
"bottom": 6
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#dfe2f1",
|
||||
"size": 12,
|
||||
"background": "#202746",
|
||||
"padding": 4,
|
||||
"corner_radius": 6,
|
||||
"margin": {
|
||||
"left": 6
|
||||
},
|
||||
"hover": {
|
||||
"background": "#2932563d"
|
||||
}
|
||||
},
|
||||
"dismiss_button": {
|
||||
"color": "#979db4",
|
||||
"icon_width": 8,
|
||||
"icon_height": 8,
|
||||
"button_width": 8,
|
||||
"button_height": 8,
|
||||
"hover": {
|
||||
"color": "#dfe2f1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -341,6 +341,19 @@
|
||||
"icon_color": "#202746",
|
||||
"background": "#979db42e"
|
||||
}
|
||||
},
|
||||
"badge": {
|
||||
"corner_radius": 3,
|
||||
"padding": 2,
|
||||
"margin": {
|
||||
"bottom": -1,
|
||||
"right": -1
|
||||
},
|
||||
"border": {
|
||||
"width": 1,
|
||||
"color": "#dfe2f1"
|
||||
},
|
||||
"background": "#3d8fd1"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -470,6 +483,33 @@
|
||||
"color": "#202746",
|
||||
"size": 14,
|
||||
"background": "#000000aa"
|
||||
},
|
||||
"notification": {
|
||||
"margin": {
|
||||
"top": 10
|
||||
},
|
||||
"background": "#dfe2f1",
|
||||
"corner_radius": 6,
|
||||
"padding": 12,
|
||||
"border": {
|
||||
"color": "#f5f7ff",
|
||||
"width": 1
|
||||
},
|
||||
"shadow": {
|
||||
"blur": 16,
|
||||
"color": "#0000001f",
|
||||
"offset": [
|
||||
0,
|
||||
2
|
||||
]
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"width": 380,
|
||||
"margin": {
|
||||
"right": 10,
|
||||
"bottom": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
@ -1639,5 +1679,56 @@
|
||||
"padding": {
|
||||
"left": 6
|
||||
}
|
||||
},
|
||||
"contact_notification": {
|
||||
"header_avatar": {
|
||||
"height": 12,
|
||||
"width": 12,
|
||||
"corner_radius": 6
|
||||
},
|
||||
"header_message": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#293256",
|
||||
"size": 12,
|
||||
"margin": {
|
||||
"left": 8,
|
||||
"right": 8
|
||||
}
|
||||
},
|
||||
"header_height": 18,
|
||||
"body_message": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#5e6687",
|
||||
"size": 12,
|
||||
"margin": {
|
||||
"left": 20,
|
||||
"top": 6,
|
||||
"bottom": 6
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"family": "Zed Sans",
|
||||
"color": "#293256",
|
||||
"size": 12,
|
||||
"background": "#f5f7ff",
|
||||
"padding": 4,
|
||||
"corner_radius": 6,
|
||||
"margin": {
|
||||
"left": 6
|
||||
},
|
||||
"hover": {
|
||||
"background": "#dfe2f11f"
|
||||
}
|
||||
},
|
||||
"dismiss_button": {
|
||||
"color": "#5e6687",
|
||||
"icon_width": 8,
|
||||
"icon_height": 8,
|
||||
"button_width": 8,
|
||||
"button_height": 8,
|
||||
"hover": {
|
||||
"color": "#293256"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -69,7 +69,7 @@ impl ChatPanel {
|
||||
.with_style(move |cx| {
|
||||
let theme = &cx.global::<Settings>().theme.chat_panel.channel_select;
|
||||
SelectStyle {
|
||||
header: theme.header.container.clone(),
|
||||
header: theme.header.container,
|
||||
menu: theme.menu.clone(),
|
||||
}
|
||||
})
|
||||
|
@ -54,10 +54,21 @@ pub struct UserStore {
|
||||
_maintain_current_user: Task<()>,
|
||||
}
|
||||
|
||||
pub enum Event {}
|
||||
#[derive(Clone)]
|
||||
pub struct ContactEvent {
|
||||
pub user: Arc<User>,
|
||||
pub kind: ContactEventKind,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum ContactEventKind {
|
||||
Requested,
|
||||
Accepted,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl Entity for UserStore {
|
||||
type Event = Event;
|
||||
type Event = ContactEvent;
|
||||
}
|
||||
|
||||
enum UpdateContacts {
|
||||
@ -175,19 +186,23 @@ impl UserStore {
|
||||
// No need to paralellize here
|
||||
let mut updated_contacts = Vec::new();
|
||||
for contact in message.contacts {
|
||||
updated_contacts.push(Arc::new(
|
||||
Contact::from_proto(contact, &this, &mut cx).await?,
|
||||
let should_notify = contact.should_notify;
|
||||
updated_contacts.push((
|
||||
Arc::new(Contact::from_proto(contact, &this, &mut cx).await?),
|
||||
should_notify,
|
||||
));
|
||||
}
|
||||
|
||||
let mut incoming_requests = Vec::new();
|
||||
for request in message.incoming_requests {
|
||||
incoming_requests.push(
|
||||
this.update(&mut cx, |this, cx| {
|
||||
incoming_requests.push({
|
||||
let user = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.fetch_user(request.requester_id, cx)
|
||||
})
|
||||
.await?,
|
||||
);
|
||||
.await?;
|
||||
(user, request.should_notify)
|
||||
});
|
||||
}
|
||||
|
||||
let mut outgoing_requests = Vec::new();
|
||||
@ -210,7 +225,13 @@ impl UserStore {
|
||||
this.contacts
|
||||
.retain(|contact| !removed_contacts.contains(&contact.user.id));
|
||||
// Update existing contacts and insert new ones
|
||||
for updated_contact in updated_contacts {
|
||||
for (updated_contact, should_notify) in updated_contacts {
|
||||
if should_notify {
|
||||
cx.emit(ContactEvent {
|
||||
user: updated_contact.user.clone(),
|
||||
kind: ContactEventKind::Accepted,
|
||||
});
|
||||
}
|
||||
match this.contacts.binary_search_by_key(
|
||||
&&updated_contact.user.github_login,
|
||||
|contact| &contact.user.github_login,
|
||||
@ -221,17 +242,33 @@ impl UserStore {
|
||||
}
|
||||
|
||||
// Remove incoming contact requests
|
||||
this.incoming_contact_requests
|
||||
.retain(|user| !removed_incoming_requests.contains(&user.id));
|
||||
this.incoming_contact_requests.retain(|user| {
|
||||
if removed_incoming_requests.contains(&user.id) {
|
||||
cx.emit(ContactEvent {
|
||||
user: user.clone(),
|
||||
kind: ContactEventKind::Cancelled,
|
||||
});
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
// Update existing incoming requests and insert new ones
|
||||
for request in incoming_requests {
|
||||
for (user, should_notify) in incoming_requests {
|
||||
if should_notify {
|
||||
cx.emit(ContactEvent {
|
||||
user: user.clone(),
|
||||
kind: ContactEventKind::Requested,
|
||||
});
|
||||
}
|
||||
|
||||
match this
|
||||
.incoming_contact_requests
|
||||
.binary_search_by_key(&&request.github_login, |contact| {
|
||||
.binary_search_by_key(&&user.github_login, |contact| {
|
||||
&contact.github_login
|
||||
}) {
|
||||
Ok(ix) => this.incoming_contact_requests[ix] = request,
|
||||
Err(ix) => this.incoming_contact_requests.insert(ix, request),
|
||||
Ok(ix) => this.incoming_contact_requests[ix] = user,
|
||||
Err(ix) => this.incoming_contact_requests.insert(ix, user),
|
||||
}
|
||||
}
|
||||
|
||||
@ -334,13 +371,31 @@ impl UserStore {
|
||||
response: if accept {
|
||||
proto::ContactRequestResponse::Accept
|
||||
} else {
|
||||
proto::ContactRequestResponse::Reject
|
||||
proto::ContactRequestResponse::Decline
|
||||
} as i32,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn dismiss_contact_request(
|
||||
&mut self,
|
||||
requester_id: u64,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let client = self.client.upgrade();
|
||||
cx.spawn_weak(|_, _| async move {
|
||||
client
|
||||
.ok_or_else(|| anyhow!("can't upgrade client reference"))?
|
||||
.request(proto::RespondToContactRequest {
|
||||
requester_id,
|
||||
response: proto::ContactRequestResponse::Dismiss as i32,
|
||||
})
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn perform_contact_request<T: RequestMessage>(
|
||||
&mut self,
|
||||
user_id: u64,
|
||||
|
@ -17,10 +17,11 @@ pub trait Db: Send + Sync {
|
||||
async fn set_user_is_admin(&self, id: UserId, is_admin: bool) -> Result<()>;
|
||||
async fn destroy_user(&self, id: UserId) -> Result<()>;
|
||||
|
||||
async fn get_contacts(&self, id: UserId) -> Result<Contacts>;
|
||||
async fn get_contacts(&self, id: UserId) -> Result<Vec<Contact>>;
|
||||
async fn has_contact(&self, user_id_a: UserId, user_id_b: UserId) -> Result<bool>;
|
||||
async fn send_contact_request(&self, requester_id: UserId, responder_id: UserId) -> Result<()>;
|
||||
async fn remove_contact(&self, requester_id: UserId, responder_id: UserId) -> Result<()>;
|
||||
async fn dismiss_contact_request(
|
||||
async fn dismiss_contact_notification(
|
||||
&self,
|
||||
responder_id: UserId,
|
||||
requester_id: UserId,
|
||||
@ -190,7 +191,7 @@ impl Db for PostgresDb {
|
||||
|
||||
// contacts
|
||||
|
||||
async fn get_contacts(&self, user_id: UserId) -> Result<Contacts> {
|
||||
async fn get_contacts(&self, user_id: UserId) -> Result<Vec<Contact>> {
|
||||
let query = "
|
||||
SELECT user_id_a, user_id_b, a_to_b, accepted, should_notify
|
||||
FROM contacts
|
||||
@ -201,46 +202,67 @@ impl Db for PostgresDb {
|
||||
.bind(user_id)
|
||||
.fetch(&self.pool);
|
||||
|
||||
let mut current = vec![user_id];
|
||||
let mut outgoing_requests = Vec::new();
|
||||
let mut incoming_requests = Vec::new();
|
||||
let mut contacts = vec![Contact::Accepted {
|
||||
user_id,
|
||||
should_notify: false,
|
||||
}];
|
||||
while let Some(row) = rows.next().await {
|
||||
let (user_id_a, user_id_b, a_to_b, accepted, should_notify) = row?;
|
||||
|
||||
if user_id_a == user_id {
|
||||
if accepted {
|
||||
current.push(user_id_b);
|
||||
contacts.push(Contact::Accepted {
|
||||
user_id: user_id_b,
|
||||
should_notify: should_notify && a_to_b,
|
||||
});
|
||||
} else if a_to_b {
|
||||
outgoing_requests.push(user_id_b);
|
||||
contacts.push(Contact::Outgoing { user_id: user_id_b })
|
||||
} else {
|
||||
incoming_requests.push(IncomingContactRequest {
|
||||
requester_id: user_id_b,
|
||||
contacts.push(Contact::Incoming {
|
||||
user_id: user_id_b,
|
||||
should_notify,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if accepted {
|
||||
current.push(user_id_a);
|
||||
contacts.push(Contact::Accepted {
|
||||
user_id: user_id_a,
|
||||
should_notify: should_notify && !a_to_b,
|
||||
});
|
||||
} else if a_to_b {
|
||||
incoming_requests.push(IncomingContactRequest {
|
||||
requester_id: user_id_a,
|
||||
contacts.push(Contact::Incoming {
|
||||
user_id: user_id_a,
|
||||
should_notify,
|
||||
});
|
||||
} else {
|
||||
outgoing_requests.push(user_id_a);
|
||||
contacts.push(Contact::Outgoing { user_id: user_id_a });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
current.sort_unstable();
|
||||
outgoing_requests.sort_unstable();
|
||||
incoming_requests.sort_unstable();
|
||||
contacts.sort_unstable_by_key(|contact| contact.user_id());
|
||||
|
||||
Ok(Contacts {
|
||||
current,
|
||||
outgoing_requests,
|
||||
incoming_requests,
|
||||
})
|
||||
Ok(contacts)
|
||||
}
|
||||
|
||||
async fn has_contact(&self, user_id_1: UserId, user_id_2: UserId) -> Result<bool> {
|
||||
let (id_a, id_b) = if user_id_1 < user_id_2 {
|
||||
(user_id_1, user_id_2)
|
||||
} else {
|
||||
(user_id_2, user_id_1)
|
||||
};
|
||||
|
||||
let query = "
|
||||
SELECT 1 FROM contacts
|
||||
WHERE user_id_a = $1 AND user_id_b = $2 AND accepted = 't'
|
||||
LIMIT 1
|
||||
";
|
||||
Ok(sqlx::query_scalar::<_, i32>(query)
|
||||
.bind(id_a.0)
|
||||
.bind(id_b.0)
|
||||
.fetch_optional(&self.pool)
|
||||
.await?
|
||||
.is_some())
|
||||
}
|
||||
|
||||
async fn send_contact_request(&self, sender_id: UserId, receiver_id: UserId) -> Result<()> {
|
||||
@ -254,7 +276,8 @@ impl Db for PostgresDb {
|
||||
VALUES ($1, $2, $3, 'f', 't')
|
||||
ON CONFLICT (user_id_a, user_id_b) DO UPDATE
|
||||
SET
|
||||
accepted = 't'
|
||||
accepted = 't',
|
||||
should_notify = 'f'
|
||||
WHERE
|
||||
NOT contacts.accepted AND
|
||||
((contacts.a_to_b = excluded.a_to_b AND contacts.user_id_a = excluded.user_id_b) OR
|
||||
@ -297,21 +320,26 @@ impl Db for PostgresDb {
|
||||
}
|
||||
}
|
||||
|
||||
async fn dismiss_contact_request(
|
||||
async fn dismiss_contact_notification(
|
||||
&self,
|
||||
responder_id: UserId,
|
||||
requester_id: UserId,
|
||||
user_id: UserId,
|
||||
contact_user_id: UserId,
|
||||
) -> Result<()> {
|
||||
let (id_a, id_b, a_to_b) = if responder_id < requester_id {
|
||||
(responder_id, requester_id, false)
|
||||
let (id_a, id_b, a_to_b) = if user_id < contact_user_id {
|
||||
(user_id, contact_user_id, true)
|
||||
} else {
|
||||
(requester_id, responder_id, true)
|
||||
(contact_user_id, user_id, false)
|
||||
};
|
||||
|
||||
let query = "
|
||||
UPDATE contacts
|
||||
SET should_notify = 'f'
|
||||
WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3;
|
||||
WHERE
|
||||
user_id_a = $1 AND user_id_b = $2 AND
|
||||
(
|
||||
(a_to_b = $3 AND accepted) OR
|
||||
(a_to_b != $3 AND NOT accepted)
|
||||
);
|
||||
";
|
||||
|
||||
let result = sqlx::query(query)
|
||||
@ -342,7 +370,7 @@ impl Db for PostgresDb {
|
||||
let result = if accept {
|
||||
let query = "
|
||||
UPDATE contacts
|
||||
SET accepted = 't', should_notify = 'f'
|
||||
SET accepted = 't', should_notify = 't'
|
||||
WHERE user_id_a = $1 AND user_id_b = $2 AND a_to_b = $3;
|
||||
";
|
||||
sqlx::query(query)
|
||||
@ -702,10 +730,28 @@ pub struct ChannelMessage {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Contacts {
|
||||
pub current: Vec<UserId>,
|
||||
pub incoming_requests: Vec<IncomingContactRequest>,
|
||||
pub outgoing_requests: Vec<UserId>,
|
||||
pub enum Contact {
|
||||
Accepted {
|
||||
user_id: UserId,
|
||||
should_notify: bool,
|
||||
},
|
||||
Outgoing {
|
||||
user_id: UserId,
|
||||
},
|
||||
Incoming {
|
||||
user_id: UserId,
|
||||
should_notify: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Contact {
|
||||
pub fn user_id(&self) -> UserId {
|
||||
match self {
|
||||
Contact::Accepted { user_id, .. } => *user_id,
|
||||
Contact::Outgoing { user_id } => *user_id,
|
||||
Contact::Incoming { user_id, .. } => *user_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
@ -947,51 +993,60 @@ pub mod tests {
|
||||
// User starts with no contacts
|
||||
assert_eq!(
|
||||
db.get_contacts(user_1).await.unwrap(),
|
||||
Contacts {
|
||||
current: vec![user_1],
|
||||
outgoing_requests: vec![],
|
||||
incoming_requests: vec![],
|
||||
},
|
||||
vec![Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
}],
|
||||
);
|
||||
|
||||
// User requests a contact. Both users see the pending request.
|
||||
db.send_contact_request(user_1, user_2).await.unwrap();
|
||||
assert!(!db.has_contact(user_1, user_2).await.unwrap());
|
||||
assert!(!db.has_contact(user_2, user_1).await.unwrap());
|
||||
assert_eq!(
|
||||
db.get_contacts(user_1).await.unwrap(),
|
||||
Contacts {
|
||||
current: vec![user_1],
|
||||
outgoing_requests: vec![user_2],
|
||||
incoming_requests: vec![],
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Outgoing { user_id: user_2 }
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_contacts(user_2).await.unwrap(),
|
||||
Contacts {
|
||||
current: vec![user_2],
|
||||
outgoing_requests: vec![],
|
||||
incoming_requests: vec![IncomingContactRequest {
|
||||
requester_id: user_1,
|
||||
&[
|
||||
Contact::Incoming {
|
||||
user_id: user_1,
|
||||
should_notify: true
|
||||
}],
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: false
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// User 2 dismisses the contact request notification without accepting or rejecting.
|
||||
// We shouldn't notify them again.
|
||||
db.dismiss_contact_request(user_1, user_2)
|
||||
db.dismiss_contact_notification(user_1, user_2)
|
||||
.await
|
||||
.unwrap_err();
|
||||
db.dismiss_contact_request(user_2, user_1).await.unwrap();
|
||||
db.dismiss_contact_notification(user_2, user_1)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
db.get_contacts(user_2).await.unwrap(),
|
||||
Contacts {
|
||||
current: vec![user_2],
|
||||
outgoing_requests: vec![],
|
||||
incoming_requests: vec![IncomingContactRequest {
|
||||
requester_id: user_1,
|
||||
&[
|
||||
Contact::Incoming {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
}],
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: false
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// User can't accept their own contact request
|
||||
@ -1005,44 +1060,106 @@ pub mod tests {
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
db.get_contacts(user_1).await.unwrap(),
|
||||
Contacts {
|
||||
current: vec![user_1, user_2],
|
||||
outgoing_requests: vec![],
|
||||
incoming_requests: vec![],
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: true
|
||||
}
|
||||
],
|
||||
);
|
||||
assert!(db.has_contact(user_1, user_2).await.unwrap());
|
||||
assert!(db.has_contact(user_2, user_1).await.unwrap());
|
||||
assert_eq!(
|
||||
db.get_contacts(user_2).await.unwrap(),
|
||||
Contacts {
|
||||
current: vec![user_1, user_2],
|
||||
outgoing_requests: vec![],
|
||||
incoming_requests: vec![],
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false,
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: false,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Users cannot re-request existing contacts.
|
||||
db.send_contact_request(user_1, user_2).await.unwrap_err();
|
||||
db.send_contact_request(user_2, user_1).await.unwrap_err();
|
||||
|
||||
// Users can't dismiss notifications of them accepting other users' requests.
|
||||
db.dismiss_contact_notification(user_2, user_1)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
db.get_contacts(user_1).await.unwrap(),
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Users can dismiss notifications of other users accepting their requests.
|
||||
db.dismiss_contact_notification(user_1, user_2)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
db.get_contacts(user_1).await.unwrap(),
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: false,
|
||||
},
|
||||
]
|
||||
);
|
||||
|
||||
// Users send each other concurrent contact requests and
|
||||
// see that they are immediately accepted.
|
||||
db.send_contact_request(user_1, user_3).await.unwrap();
|
||||
db.send_contact_request(user_3, user_1).await.unwrap();
|
||||
assert_eq!(
|
||||
db.get_contacts(user_1).await.unwrap(),
|
||||
Contacts {
|
||||
current: vec![user_1, user_2, user_3],
|
||||
outgoing_requests: vec![],
|
||||
incoming_requests: vec![],
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: false,
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_3,
|
||||
should_notify: false
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_contacts(user_3).await.unwrap(),
|
||||
Contacts {
|
||||
current: vec![user_1, user_3],
|
||||
outgoing_requests: vec![],
|
||||
incoming_requests: vec![],
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_3,
|
||||
should_notify: false
|
||||
}
|
||||
],
|
||||
);
|
||||
|
||||
// User declines a contact request. Both users see that it is gone.
|
||||
@ -1050,21 +1167,33 @@ pub mod tests {
|
||||
db.respond_to_contact_request(user_3, user_2, false)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!db.has_contact(user_2, user_3).await.unwrap());
|
||||
assert!(!db.has_contact(user_3, user_2).await.unwrap());
|
||||
assert_eq!(
|
||||
db.get_contacts(user_2).await.unwrap(),
|
||||
Contacts {
|
||||
current: vec![user_1, user_2],
|
||||
outgoing_requests: vec![],
|
||||
incoming_requests: vec![],
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_2,
|
||||
should_notify: false
|
||||
}
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
db.get_contacts(user_3).await.unwrap(),
|
||||
Contacts {
|
||||
current: vec![user_1, user_3],
|
||||
outgoing_requests: vec![],
|
||||
incoming_requests: vec![],
|
||||
&[
|
||||
Contact::Accepted {
|
||||
user_id: user_1,
|
||||
should_notify: false
|
||||
},
|
||||
Contact::Accepted {
|
||||
user_id: user_3,
|
||||
should_notify: false
|
||||
}
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1219,40 +1348,51 @@ pub mod tests {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
async fn get_contacts(&self, id: UserId) -> Result<Contacts> {
|
||||
async fn get_contacts(&self, id: UserId) -> Result<Vec<Contact>> {
|
||||
self.background.simulate_random_delay().await;
|
||||
let mut current = vec![id];
|
||||
let mut outgoing_requests = Vec::new();
|
||||
let mut incoming_requests = Vec::new();
|
||||
let mut contacts = vec![Contact::Accepted {
|
||||
user_id: id,
|
||||
should_notify: false,
|
||||
}];
|
||||
|
||||
for contact in self.contacts.lock().iter() {
|
||||
if contact.requester_id == id {
|
||||
if contact.accepted {
|
||||
current.push(contact.responder_id);
|
||||
contacts.push(Contact::Accepted {
|
||||
user_id: contact.responder_id,
|
||||
should_notify: contact.should_notify,
|
||||
});
|
||||
} else {
|
||||
outgoing_requests.push(contact.responder_id);
|
||||
contacts.push(Contact::Outgoing {
|
||||
user_id: contact.responder_id,
|
||||
});
|
||||
}
|
||||
} else if contact.responder_id == id {
|
||||
if contact.accepted {
|
||||
current.push(contact.requester_id);
|
||||
contacts.push(Contact::Accepted {
|
||||
user_id: contact.requester_id,
|
||||
should_notify: false,
|
||||
});
|
||||
} else {
|
||||
incoming_requests.push(IncomingContactRequest {
|
||||
requester_id: contact.requester_id,
|
||||
contacts.push(Contact::Incoming {
|
||||
user_id: contact.requester_id,
|
||||
should_notify: contact.should_notify,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
current.sort_unstable();
|
||||
outgoing_requests.sort_unstable();
|
||||
incoming_requests.sort_unstable();
|
||||
contacts.sort_unstable_by_key(|contact| contact.user_id());
|
||||
Ok(contacts)
|
||||
}
|
||||
|
||||
Ok(Contacts {
|
||||
current,
|
||||
outgoing_requests,
|
||||
incoming_requests,
|
||||
})
|
||||
async fn has_contact(&self, user_id_a: UserId, user_id_b: UserId) -> Result<bool> {
|
||||
self.background.simulate_random_delay().await;
|
||||
Ok(self.contacts.lock().iter().any(|contact| {
|
||||
contact.accepted
|
||||
&& ((contact.requester_id == user_id_a && contact.responder_id == user_id_b)
|
||||
|| (contact.requester_id == user_id_b && contact.responder_id == user_id_a))
|
||||
}))
|
||||
}
|
||||
|
||||
async fn send_contact_request(
|
||||
@ -1274,6 +1414,7 @@ pub mod tests {
|
||||
Err(anyhow!("contact already exists"))?;
|
||||
} else {
|
||||
contact.accepted = true;
|
||||
contact.should_notify = false;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@ -1294,22 +1435,29 @@ pub mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn dismiss_contact_request(
|
||||
async fn dismiss_contact_notification(
|
||||
&self,
|
||||
responder_id: UserId,
|
||||
requester_id: UserId,
|
||||
user_id: UserId,
|
||||
contact_user_id: UserId,
|
||||
) -> Result<()> {
|
||||
let mut contacts = self.contacts.lock();
|
||||
for contact in contacts.iter_mut() {
|
||||
if contact.requester_id == requester_id && contact.responder_id == responder_id {
|
||||
if contact.accepted {
|
||||
return Err(anyhow!("contact already confirmed"));
|
||||
if contact.requester_id == contact_user_id
|
||||
&& contact.responder_id == user_id
|
||||
&& !contact.accepted
|
||||
{
|
||||
contact.should_notify = false;
|
||||
return Ok(());
|
||||
}
|
||||
if contact.requester_id == user_id
|
||||
&& contact.responder_id == contact_user_id
|
||||
&& contact.accepted
|
||||
{
|
||||
contact.should_notify = false;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(anyhow!("no such contact request"))
|
||||
Err(anyhow!("no such notification"))
|
||||
}
|
||||
|
||||
async fn respond_to_contact_request(
|
||||
@ -1326,6 +1474,7 @@ pub mod tests {
|
||||
}
|
||||
if accept {
|
||||
contact.accepted = true;
|
||||
contact.should_notify = true;
|
||||
} else {
|
||||
contacts.remove(ix);
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ mod store;
|
||||
|
||||
use crate::{
|
||||
auth,
|
||||
db::{ChannelId, MessageId, UserId},
|
||||
db::{self, ChannelId, MessageId, UserId},
|
||||
AppState, Result,
|
||||
};
|
||||
use anyhow::anyhow;
|
||||
@ -420,8 +420,13 @@ impl Server {
|
||||
async fn update_user_contacts(self: &Arc<Server>, user_id: UserId) -> Result<()> {
|
||||
let contacts = self.app_state.db.get_contacts(user_id).await?;
|
||||
let store = self.store().await;
|
||||
let updated_contact = store.contact_for_user(user_id);
|
||||
for contact_user_id in contacts.current {
|
||||
let updated_contact = store.contact_for_user(user_id, false);
|
||||
for contact in contacts {
|
||||
if let db::Contact::Accepted {
|
||||
user_id: contact_user_id,
|
||||
..
|
||||
} = contact
|
||||
{
|
||||
for contact_conn_id in store.connection_ids_for_user(contact_user_id) {
|
||||
self.peer
|
||||
.send(
|
||||
@ -438,6 +443,7 @@ impl Server {
|
||||
.trace_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -473,8 +479,12 @@ impl Server {
|
||||
guest_user_id = state.user_id_for_connection(request.sender_id)?;
|
||||
};
|
||||
|
||||
let guest_contacts = self.app_state.db.get_contacts(guest_user_id).await?;
|
||||
if !guest_contacts.current.contains(&host_user_id) {
|
||||
let has_contact = self
|
||||
.app_state
|
||||
.db
|
||||
.has_contact(guest_user_id, host_user_id)
|
||||
.await?;
|
||||
if !has_contact {
|
||||
return Err(anyhow!("no such project"))?;
|
||||
}
|
||||
|
||||
@ -1023,6 +1033,12 @@ impl Server {
|
||||
.await
|
||||
.user_id_for_connection(request.sender_id)?;
|
||||
let requester_id = UserId::from_proto(request.payload.requester_id);
|
||||
if request.payload.response == proto::ContactRequestResponse::Dismiss as i32 {
|
||||
self.app_state
|
||||
.db
|
||||
.dismiss_contact_notification(responder_id, requester_id)
|
||||
.await?;
|
||||
} else {
|
||||
let accept = request.payload.response == proto::ContactRequestResponse::Accept as i32;
|
||||
self.app_state
|
||||
.db
|
||||
@ -1033,7 +1049,9 @@ impl Server {
|
||||
// Update responder with new contact
|
||||
let mut update = proto::UpdateContacts::default();
|
||||
if accept {
|
||||
update.contacts.push(store.contact_for_user(requester_id));
|
||||
update
|
||||
.contacts
|
||||
.push(store.contact_for_user(requester_id, false));
|
||||
}
|
||||
update
|
||||
.remove_incoming_requests
|
||||
@ -1045,7 +1063,9 @@ impl Server {
|
||||
// Update requester with new contact
|
||||
let mut update = proto::UpdateContacts::default();
|
||||
if accept {
|
||||
update.contacts.push(store.contact_for_user(responder_id));
|
||||
update
|
||||
.contacts
|
||||
.push(store.contact_for_user(responder_id, true));
|
||||
}
|
||||
update
|
||||
.remove_outgoing_requests
|
||||
@ -1053,6 +1073,7 @@ impl Server {
|
||||
for connection_id in store.connection_ids_for_user(requester_id) {
|
||||
self.peer.send(connection_id, update.clone())?;
|
||||
}
|
||||
}
|
||||
|
||||
response.send(proto::Ack {})?;
|
||||
Ok(())
|
||||
@ -7245,7 +7266,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut gpui::RenderContext<Self>) -> gpui::ElementBox {
|
||||
gpui::Element::boxed(gpui::elements::Empty)
|
||||
gpui::Element::boxed(gpui::elements::Empty::new())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -217,33 +217,46 @@ impl Store {
|
||||
.is_empty()
|
||||
}
|
||||
|
||||
pub fn build_initial_contacts_update(&self, contacts: db::Contacts) -> proto::UpdateContacts {
|
||||
pub fn build_initial_contacts_update(
|
||||
&self,
|
||||
contacts: Vec<db::Contact>,
|
||||
) -> proto::UpdateContacts {
|
||||
let mut update = proto::UpdateContacts::default();
|
||||
for user_id in contacts.current {
|
||||
update.contacts.push(self.contact_for_user(user_id));
|
||||
}
|
||||
|
||||
for request in contacts.incoming_requests {
|
||||
for contact in contacts {
|
||||
match contact {
|
||||
db::Contact::Accepted {
|
||||
user_id,
|
||||
should_notify,
|
||||
} => {
|
||||
update
|
||||
.contacts
|
||||
.push(self.contact_for_user(user_id, should_notify));
|
||||
}
|
||||
db::Contact::Outgoing { user_id } => {
|
||||
update.outgoing_requests.push(user_id.to_proto())
|
||||
}
|
||||
db::Contact::Incoming {
|
||||
user_id,
|
||||
should_notify,
|
||||
} => update
|
||||
.incoming_requests
|
||||
.push(proto::IncomingContactRequest {
|
||||
requester_id: request.requester_id.to_proto(),
|
||||
should_notify: request.should_notify,
|
||||
})
|
||||
requester_id: user_id.to_proto(),
|
||||
should_notify,
|
||||
}),
|
||||
}
|
||||
|
||||
for requested_user_id in contacts.outgoing_requests {
|
||||
update.outgoing_requests.push(requested_user_id.to_proto())
|
||||
}
|
||||
|
||||
update
|
||||
}
|
||||
|
||||
pub fn contact_for_user(&self, user_id: UserId) -> proto::Contact {
|
||||
pub fn contact_for_user(&self, user_id: UserId, should_notify: bool) -> proto::Contact {
|
||||
proto::Contact {
|
||||
user_id: user_id.to_proto(),
|
||||
projects: self.project_metadata_for_user(user_id),
|
||||
online: self.is_user_online(user_id),
|
||||
should_notify,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,7 @@ impl CommandPalette {
|
||||
cx.as_mut().defer(move |cx| {
|
||||
let this = cx.add_view(window_id, |cx| Self::new(focused_view_id, cx));
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_modal(cx, |cx, _| {
|
||||
workspace.toggle_modal(cx, |_, cx| {
|
||||
cx.subscribe(&this, Self::on_event).detach();
|
||||
this
|
||||
});
|
||||
|
@ -118,7 +118,7 @@ impl PickerDelegate for ContactFinder {
|
||||
"icons/accept.svg"
|
||||
}
|
||||
ContactRequestStatus::RequestSent | ContactRequestStatus::RequestAccepted => {
|
||||
"icons/reject.svg"
|
||||
"icons/decline.svg"
|
||||
}
|
||||
};
|
||||
let button_style = if self.user_store.read(cx).is_contact_request_pending(&user) {
|
||||
@ -159,7 +159,7 @@ impl PickerDelegate for ContactFinder {
|
||||
|
||||
impl ContactFinder {
|
||||
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.toggle_modal(cx, |cx, workspace| {
|
||||
workspace.toggle_modal(cx, |workspace, cx| {
|
||||
let finder = cx.add_view(|cx| Self::new(workspace.user_store().clone(), cx));
|
||||
cx.subscribe(&finder, Self::on_event).detach();
|
||||
finder
|
||||
|
237
crates/contacts_panel/src/contact_notification.rs
Normal file
237
crates/contacts_panel/src/contact_notification.rs
Normal file
@ -0,0 +1,237 @@
|
||||
use client::{ContactEvent, ContactEventKind, UserStore};
|
||||
use gpui::{
|
||||
elements::*, impl_internal_actions, platform::CursorStyle, Entity, ModelHandle,
|
||||
MutableAppContext, RenderContext, View, ViewContext,
|
||||
};
|
||||
use settings::Settings;
|
||||
use workspace::Notification;
|
||||
|
||||
use crate::render_icon_button;
|
||||
|
||||
impl_internal_actions!(contact_notifications, [Dismiss, RespondToContactRequest]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ContactNotification::dismiss);
|
||||
cx.add_action(ContactNotification::respond_to_contact_request);
|
||||
}
|
||||
|
||||
pub struct ContactNotification {
|
||||
user_store: ModelHandle<UserStore>,
|
||||
event: ContactEvent,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Dismiss(u64);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RespondToContactRequest {
|
||||
pub user_id: u64,
|
||||
pub accept: bool,
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Dismiss,
|
||||
}
|
||||
|
||||
enum Decline {}
|
||||
enum Accept {}
|
||||
|
||||
impl Entity for ContactNotification {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for ContactNotification {
|
||||
fn ui_name() -> &'static str {
|
||||
"ContactNotification"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
match self.event.kind {
|
||||
ContactEventKind::Requested => self.render_incoming_request(cx),
|
||||
ContactEventKind::Accepted => self.render_acceptance(cx),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Notification for ContactNotification {
|
||||
fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
|
||||
matches!(event, Event::Dismiss)
|
||||
}
|
||||
}
|
||||
|
||||
impl ContactNotification {
|
||||
pub fn new(
|
||||
event: ContactEvent,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.subscribe(&user_store, move |this, _, event, cx| {
|
||||
if let client::ContactEvent {
|
||||
kind: ContactEventKind::Cancelled,
|
||||
user,
|
||||
} = event
|
||||
{
|
||||
if user.id == this.event.user.id {
|
||||
cx.emit(Event::Dismiss);
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self { event, user_store }
|
||||
}
|
||||
|
||||
fn render_incoming_request(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let theme = &theme.contact_notification;
|
||||
let user = &self.event.user;
|
||||
let user_id = user.id;
|
||||
|
||||
Flex::column()
|
||||
.with_child(self.render_header("wants to add you as a contact.", theme, cx))
|
||||
.with_child(
|
||||
Label::new(
|
||||
"They won't know if you decline.".to_string(),
|
||||
theme.body_message.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.body_message.container)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Decline, _, _>(
|
||||
self.event.user.id as usize,
|
||||
cx,
|
||||
|state, _| {
|
||||
let button = theme.button.style_for(state, false);
|
||||
Label::new("Decline".to_string(), button.text.clone())
|
||||
.contained()
|
||||
.with_style(button.container)
|
||||
.boxed()
|
||||
},
|
||||
)
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, cx| {
|
||||
cx.dispatch_action(RespondToContactRequest {
|
||||
user_id,
|
||||
accept: false,
|
||||
});
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Accept, _, _>(user.id as usize, cx, |state, _| {
|
||||
let button = theme.button.style_for(state, false);
|
||||
Label::new("Accept".to_string(), button.text.clone())
|
||||
.contained()
|
||||
.with_style(button.container)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(move |_, cx| {
|
||||
cx.dispatch_action(RespondToContactRequest {
|
||||
user_id,
|
||||
accept: true,
|
||||
});
|
||||
})
|
||||
.boxed(),
|
||||
)
|
||||
.aligned()
|
||||
.right()
|
||||
.boxed(),
|
||||
)
|
||||
.contained()
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_acceptance(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
let theme = cx.global::<Settings>().theme.clone();
|
||||
let theme = &theme.contact_notification;
|
||||
|
||||
self.render_header("accepted your contact request", theme, cx)
|
||||
}
|
||||
|
||||
fn render_header(
|
||||
&self,
|
||||
message: &'static str,
|
||||
theme: &theme::ContactNotification,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
let user = &self.event.user;
|
||||
let user_id = user.id;
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
.with_style(theme.header_avatar)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_height(
|
||||
cx.font_cache()
|
||||
.line_height(theme.header_message.text.font_size),
|
||||
)
|
||||
.aligned()
|
||||
.top()
|
||||
.boxed()
|
||||
}))
|
||||
.with_child(
|
||||
Text::new(
|
||||
format!("{} {}", user.github_login, message),
|
||||
theme.header_message.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.header_message.container)
|
||||
.aligned()
|
||||
.top()
|
||||
.left()
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::new::<Dismiss, _, _>(user.id as usize, cx, |state, _| {
|
||||
render_icon_button(
|
||||
theme.dismiss_button.style_for(state, false),
|
||||
"icons/decline.svg",
|
||||
)
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.with_padding(Padding::uniform(5.))
|
||||
.on_click(move |_, cx| cx.dispatch_action(Dismiss(user_id)))
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_height(
|
||||
cx.font_cache()
|
||||
.line_height(theme.header_message.text.font_size),
|
||||
)
|
||||
.aligned()
|
||||
.top()
|
||||
.flex_float()
|
||||
.boxed(),
|
||||
)
|
||||
.named("contact notification header")
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
|
||||
self.user_store.update(cx, |store, cx| {
|
||||
store
|
||||
.dismiss_contact_request(self.event.user.id, cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
cx.emit(Event::Dismiss);
|
||||
}
|
||||
|
||||
fn respond_to_contact_request(
|
||||
&mut self,
|
||||
action: &RespondToContactRequest,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.user_store
|
||||
.update(cx, |store, cx| {
|
||||
store.respond_to_contact_request(action.user_id, action.accept, cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
mod contact_finder;
|
||||
mod contact_notification;
|
||||
|
||||
use client::{Contact, User, UserStore};
|
||||
use client::{Contact, ContactEventKind, User, UserStore};
|
||||
use contact_notification::ContactNotification;
|
||||
use editor::{Cancel, Editor};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
@ -8,15 +10,18 @@ use gpui::{
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
impl_actions,
|
||||
platform::CursorStyle,
|
||||
Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext, RenderContext,
|
||||
Subscription, View, ViewContext, ViewHandle,
|
||||
AppContext, Element, ElementBox, Entity, LayoutContext, ModelHandle, MutableAppContext,
|
||||
RenderContext, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::sync::Arc;
|
||||
use theme::IconButton;
|
||||
use workspace::menu::{Confirm, SelectNext, SelectPrev};
|
||||
use workspace::{AppState, JoinProject};
|
||||
use workspace::{
|
||||
menu::{Confirm, SelectNext, SelectPrev},
|
||||
sidebar::SidebarItem,
|
||||
AppState, JoinProject, Workspace,
|
||||
};
|
||||
|
||||
impl_actions!(
|
||||
contacts_panel,
|
||||
@ -65,6 +70,7 @@ pub struct RespondToContactRequest {
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
contact_finder::init(cx);
|
||||
contact_notification::init(cx);
|
||||
cx.add_action(ContactsPanel::request_contact);
|
||||
cx.add_action(ContactsPanel::remove_contact);
|
||||
cx.add_action(ContactsPanel::respond_to_contact_request);
|
||||
@ -75,7 +81,11 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
}
|
||||
|
||||
impl ContactsPanel {
|
||||
pub fn new(app_state: Arc<AppState>, cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(
|
||||
app_state: Arc<AppState>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let user_query_editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::single_line(
|
||||
Some(|theme| theme.contacts_panel.user_query_editor.clone()),
|
||||
@ -93,6 +103,27 @@ impl ContactsPanel {
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe(&app_state.user_store, {
|
||||
let user_store = app_state.user_store.downgrade();
|
||||
move |_, _, event, cx| {
|
||||
if let Some((workspace, user_store)) =
|
||||
workspace.upgrade(cx).zip(user_store.upgrade(cx))
|
||||
{
|
||||
workspace.update(cx, |workspace, cx| match event.kind {
|
||||
ContactEventKind::Requested | ContactEventKind::Accepted => workspace
|
||||
.show_notification(
|
||||
cx.add_view(|cx| {
|
||||
ContactNotification::new(event.clone(), user_store, cx)
|
||||
}),
|
||||
cx,
|
||||
),
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut this = Self {
|
||||
list_state: ListState::new(0, Orientation::Top, 1000., {
|
||||
let this = cx.weak_handle();
|
||||
@ -382,7 +413,7 @@ impl ContactsPanel {
|
||||
is_selected: bool,
|
||||
cx: &mut LayoutContext,
|
||||
) -> ElementBox {
|
||||
enum Reject {}
|
||||
enum Decline {}
|
||||
enum Accept {}
|
||||
enum Cancel {}
|
||||
|
||||
@ -413,13 +444,13 @@ impl ContactsPanel {
|
||||
|
||||
if is_incoming {
|
||||
row.add_children([
|
||||
MouseEventHandler::new::<Reject, _, _>(user.id as usize, cx, |mouse_state, _| {
|
||||
MouseEventHandler::new::<Decline, _, _>(user.id as usize, cx, |mouse_state, _| {
|
||||
let button_style = if is_contact_request_pending {
|
||||
&theme.disabled_contact_button
|
||||
} else {
|
||||
&theme.contact_button.style_for(mouse_state, false)
|
||||
};
|
||||
render_icon_button(button_style, "icons/reject.svg")
|
||||
render_icon_button(button_style, "icons/decline.svg")
|
||||
.aligned()
|
||||
// .flex_float()
|
||||
.boxed()
|
||||
@ -463,7 +494,7 @@ impl ContactsPanel {
|
||||
} else {
|
||||
&theme.contact_button.style_for(mouse_state, false)
|
||||
};
|
||||
render_icon_button(button_style, "icons/reject.svg")
|
||||
render_icon_button(button_style, "icons/decline.svg")
|
||||
.aligned()
|
||||
.flex_float()
|
||||
.boxed()
|
||||
@ -707,6 +738,16 @@ impl ContactsPanel {
|
||||
}
|
||||
}
|
||||
|
||||
impl SidebarItem for ContactsPanel {
|
||||
fn should_show_badge(&self, cx: &AppContext) -> bool {
|
||||
!self
|
||||
.user_store
|
||||
.read(cx)
|
||||
.incoming_contact_requests()
|
||||
.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element {
|
||||
Svg::new(svg_path)
|
||||
.with_color(style.color)
|
||||
@ -824,11 +865,16 @@ mod tests {
|
||||
use gpui::TestAppContext;
|
||||
use language::LanguageRegistry;
|
||||
use theme::ThemeRegistry;
|
||||
use workspace::WorkspaceParams;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_contact_panel(cx: &mut TestAppContext) {
|
||||
let (app_state, server) = init(cx).await;
|
||||
let panel = cx.add_view(0, |cx| ContactsPanel::new(app_state.clone(), cx));
|
||||
let workspace_params = cx.update(WorkspaceParams::test);
|
||||
let workspace = cx.add_view(0, |cx| Workspace::new(&workspace_params, cx));
|
||||
let panel = cx.add_view(0, |cx| {
|
||||
ContactsPanel::new(app_state.clone(), workspace.downgrade(), cx)
|
||||
});
|
||||
|
||||
let get_users_request = server.receive::<proto::GetUsers>().await.unwrap();
|
||||
server
|
||||
@ -865,6 +911,7 @@ mod tests {
|
||||
proto::Contact {
|
||||
user_id: 3,
|
||||
online: true,
|
||||
should_notify: false,
|
||||
projects: vec![proto::ProjectMetadata {
|
||||
id: 101,
|
||||
worktree_root_names: vec!["dir1".to_string()],
|
||||
@ -875,6 +922,7 @@ mod tests {
|
||||
proto::Contact {
|
||||
user_id: 4,
|
||||
online: true,
|
||||
should_notify: false,
|
||||
projects: vec![proto::ProjectMetadata {
|
||||
id: 102,
|
||||
worktree_root_names: vec!["dir2".to_string()],
|
||||
@ -885,6 +933,7 @@ mod tests {
|
||||
proto::Contact {
|
||||
user_id: 5,
|
||||
online: false,
|
||||
should_notify: false,
|
||||
projects: vec![],
|
||||
},
|
||||
],
|
||||
|
@ -85,7 +85,7 @@ impl FileFinder {
|
||||
}
|
||||
|
||||
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.toggle_modal(cx, |cx, workspace| {
|
||||
workspace.toggle_modal(cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let finder = cx.add_view(|cx| Self::new(project, cx));
|
||||
cx.subscribe(&finder, Self::on_event).detach();
|
||||
|
@ -62,7 +62,7 @@ impl GoToLine {
|
||||
.active_item(cx)
|
||||
.and_then(|active_item| active_item.downcast::<Editor>())
|
||||
{
|
||||
workspace.toggle_modal(cx, |cx, _| {
|
||||
workspace.toggle_modal(cx, |_, cx| {
|
||||
let view = cx.add_view(|cx| GoToLine::new(editor, cx));
|
||||
cx.subscribe(&view, Self::on_event).detach();
|
||||
view
|
||||
|
@ -8,11 +8,18 @@ use crate::{
|
||||
};
|
||||
use crate::{Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint};
|
||||
|
||||
pub struct Empty;
|
||||
pub struct Empty {
|
||||
collapsed: bool,
|
||||
}
|
||||
|
||||
impl Empty {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
Self { collapsed: false }
|
||||
}
|
||||
|
||||
pub fn collapsed(mut self) -> Self {
|
||||
self.collapsed = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,12 +32,12 @@ impl Element for Empty {
|
||||
constraint: SizeConstraint,
|
||||
_: &mut LayoutContext,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
let x = if constraint.max.x().is_finite() {
|
||||
let x = if constraint.max.x().is_finite() && !self.collapsed {
|
||||
constraint.max.x()
|
||||
} else {
|
||||
constraint.min.x()
|
||||
};
|
||||
let y = if constraint.max.y().is_finite() {
|
||||
let y = if constraint.max.y().is_finite() && !self.collapsed {
|
||||
constraint.max.y()
|
||||
} else {
|
||||
constraint.min.y()
|
||||
|
@ -87,7 +87,7 @@ impl OutlineView {
|
||||
.read(cx)
|
||||
.outline(Some(cx.global::<Settings>().theme.editor.syntax.as_ref()));
|
||||
if let Some(outline) = buffer {
|
||||
workspace.toggle_modal(cx, |cx, _| {
|
||||
workspace.toggle_modal(cx, |_, cx| {
|
||||
let view = cx.add_view(|cx| OutlineView::new(outline, editor, cx));
|
||||
cx.subscribe(&view, Self::on_event).detach();
|
||||
view
|
||||
|
@ -900,6 +900,12 @@ impl Entity for ProjectPanel {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl workspace::sidebar::SidebarItem for ProjectPanel {
|
||||
fn should_show_badge(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -71,7 +71,7 @@ impl ProjectSymbolsView {
|
||||
}
|
||||
|
||||
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
workspace.toggle_modal(cx, |cx, workspace| {
|
||||
workspace.toggle_modal(cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let symbols = cx.add_view(|cx| Self::new(project, cx));
|
||||
cx.subscribe(&symbols, Self::on_event).detach();
|
||||
|
@ -564,8 +564,9 @@ message RespondToContactRequest {
|
||||
|
||||
enum ContactRequestResponse {
|
||||
Accept = 0;
|
||||
Reject = 1;
|
||||
Decline = 1;
|
||||
Block = 2;
|
||||
Dismiss = 3;
|
||||
}
|
||||
|
||||
message SendChannelMessage {
|
||||
@ -876,6 +877,7 @@ message Contact {
|
||||
uint64 user_id = 1;
|
||||
repeated ProjectMetadata projects = 2;
|
||||
bool online = 3;
|
||||
bool should_notify = 4;
|
||||
}
|
||||
|
||||
message ProjectMetadata {
|
||||
|
@ -29,6 +29,7 @@ pub struct Theme {
|
||||
pub search: Search,
|
||||
pub project_diagnostics: ProjectDiagnostics,
|
||||
pub breadcrumbs: ContainedText,
|
||||
pub contact_notification: ContactNotification,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
@ -45,6 +46,8 @@ pub struct Workspace {
|
||||
pub toolbar: Toolbar,
|
||||
pub disconnected_overlay: ContainedText,
|
||||
pub modal: ContainerStyle,
|
||||
pub notification: ContainerStyle,
|
||||
pub notifications: Notifications,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
@ -109,6 +112,13 @@ pub struct Toolbar {
|
||||
pub item_spacing: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Notifications {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub width: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Search {
|
||||
#[serde(flatten)]
|
||||
@ -152,6 +162,7 @@ pub struct StatusBarSidebarButtons {
|
||||
pub group_left: ContainerStyle,
|
||||
pub group_right: ContainerStyle,
|
||||
pub item: Interactive<SidebarItem>,
|
||||
pub badge: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
@ -350,6 +361,16 @@ pub struct ProjectDiagnostics {
|
||||
pub tab_summary_spacing: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct ContactNotification {
|
||||
pub header_avatar: ImageStyle,
|
||||
pub header_message: ContainedText,
|
||||
pub header_height: f32,
|
||||
pub body_message: ContainedText,
|
||||
pub button: Interactive<ContainedText>,
|
||||
pub dismiss_button: Interactive<IconButton>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct Editor {
|
||||
pub text_color: Color,
|
||||
|
@ -66,7 +66,7 @@ impl ThemeSelector {
|
||||
|
||||
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||
let themes = workspace.themes();
|
||||
workspace.toggle_modal(cx, |cx, _| {
|
||||
workspace.toggle_modal(cx, |_, cx| {
|
||||
let this = cx.add_view(|cx| Self::new(themes, cx));
|
||||
cx.subscribe(&this, Self::on_event).detach();
|
||||
this
|
||||
|
@ -61,12 +61,12 @@ pub fn marked_text_ranges(full_marked_text: &str) -> (String, Vec<Range<usize>>)
|
||||
let (range_marked_text, empty_offsets) = marked_text(full_marked_text);
|
||||
let (unmarked, range_lookup) =
|
||||
marked_text_ranges_by(&range_marked_text, vec![('[', ']'), ('(', ')'), ('<', '>')]);
|
||||
(
|
||||
unmarked,
|
||||
range_lookup
|
||||
let mut combined_ranges: Vec<_> = range_lookup
|
||||
.into_values()
|
||||
.flatten()
|
||||
.chain(empty_offsets.into_iter().map(|offset| offset..offset))
|
||||
.collect(),
|
||||
)
|
||||
.collect();
|
||||
|
||||
combined_ranges.sort_by_key(|range| range.start);
|
||||
(unmarked, combined_ranges)
|
||||
}
|
||||
|
@ -1,13 +1,40 @@
|
||||
use crate::StatusItemView;
|
||||
use gpui::{
|
||||
elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, Entity, RenderContext, View,
|
||||
ViewContext, ViewHandle,
|
||||
elements::*, impl_actions, platform::CursorStyle, AnyViewHandle, AppContext, Entity,
|
||||
RenderContext, Subscription, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use theme::Theme;
|
||||
|
||||
use crate::StatusItemView;
|
||||
pub trait SidebarItem: View {
|
||||
fn should_show_badge(&self, cx: &AppContext) -> bool;
|
||||
}
|
||||
|
||||
pub trait SidebarItemHandle {
|
||||
fn should_show_badge(&self, cx: &AppContext) -> bool;
|
||||
fn to_any(&self) -> AnyViewHandle;
|
||||
}
|
||||
|
||||
impl<T> SidebarItemHandle for ViewHandle<T>
|
||||
where
|
||||
T: SidebarItem,
|
||||
{
|
||||
fn should_show_badge(&self, cx: &AppContext) -> bool {
|
||||
self.read(cx).should_show_badge(cx)
|
||||
}
|
||||
|
||||
fn to_any(&self) -> AnyViewHandle {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<AnyViewHandle> for &dyn SidebarItemHandle {
|
||||
fn into(self) -> AnyViewHandle {
|
||||
self.to_any()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Sidebar {
|
||||
side: Side,
|
||||
@ -23,10 +50,10 @@ pub enum Side {
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Item {
|
||||
icon_path: &'static str,
|
||||
view: AnyViewHandle,
|
||||
view: Rc<dyn SidebarItemHandle>,
|
||||
_observation: Subscription,
|
||||
}
|
||||
|
||||
pub struct SidebarButtons {
|
||||
@ -58,13 +85,18 @@ impl Sidebar {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_item(
|
||||
pub fn add_item<T: SidebarItem>(
|
||||
&mut self,
|
||||
icon_path: &'static str,
|
||||
view: AnyViewHandle,
|
||||
view: ViewHandle<T>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.items.push(Item { icon_path, view });
|
||||
let subscription = cx.observe(&view, |_, _, cx| cx.notify());
|
||||
self.items.push(Item {
|
||||
icon_path,
|
||||
view: Rc::new(view),
|
||||
_observation: subscription,
|
||||
});
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
@ -82,10 +114,10 @@ impl Sidebar {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn active_item(&self) -> Option<&AnyViewHandle> {
|
||||
pub fn active_item(&self) -> Option<&dyn SidebarItemHandle> {
|
||||
self.active_item_ix
|
||||
.and_then(|ix| self.items.get(ix))
|
||||
.map(|item| &item.view)
|
||||
.map(|item| item.view.as_ref())
|
||||
}
|
||||
|
||||
fn render_resize_handle(&self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
|
||||
@ -185,20 +217,47 @@ impl View for SidebarButtons {
|
||||
.sidebar_buttons;
|
||||
let sidebar = self.sidebar.read(cx);
|
||||
let item_style = theme.item;
|
||||
let badge_style = theme.badge;
|
||||
let active_ix = sidebar.active_item_ix;
|
||||
let side = sidebar.side;
|
||||
let group_style = match side {
|
||||
Side::Left => theme.group_left,
|
||||
Side::Right => theme.group_right,
|
||||
};
|
||||
let items = sidebar.items.clone();
|
||||
let items = sidebar
|
||||
.items
|
||||
.iter()
|
||||
.map(|item| (item.icon_path, item.view.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
Flex::row()
|
||||
.with_children(items.iter().enumerate().map(|(ix, item)| {
|
||||
MouseEventHandler::new::<Self, _, _>(ix, cx, move |state, _| {
|
||||
let style = item_style.style_for(state, Some(ix) == active_ix);
|
||||
Svg::new(item.icon_path)
|
||||
.with_color(style.icon_color)
|
||||
.with_children(
|
||||
items
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, (icon_path, item_view))| {
|
||||
MouseEventHandler::new::<Self, _, _>(ix, cx, move |state, cx| {
|
||||
let is_active = Some(ix) == active_ix;
|
||||
let style = item_style.style_for(state, is_active);
|
||||
Stack::new()
|
||||
.with_child(
|
||||
Svg::new(icon_path).with_color(style.icon_color).boxed(),
|
||||
)
|
||||
.with_children(if !is_active && item_view.should_show_badge(cx) {
|
||||
Some(
|
||||
Empty::new()
|
||||
.collapsed()
|
||||
.contained()
|
||||
.with_style(badge_style)
|
||||
.aligned()
|
||||
.bottom()
|
||||
.right()
|
||||
.boxed(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.with_height(style.icon_size)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
@ -212,7 +271,8 @@ impl View for SidebarButtons {
|
||||
})
|
||||
})
|
||||
.boxed()
|
||||
}))
|
||||
}),
|
||||
)
|
||||
.contained()
|
||||
.with_style(group_style)
|
||||
.boxed()
|
||||
|
@ -604,6 +604,31 @@ impl<T: Item> WeakItemHandle for WeakViewHandle<T> {
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Notification: View {
|
||||
fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool;
|
||||
}
|
||||
|
||||
pub trait NotificationHandle {
|
||||
fn id(&self) -> usize;
|
||||
fn to_any(&self) -> AnyViewHandle;
|
||||
}
|
||||
|
||||
impl<T: Notification> NotificationHandle for ViewHandle<T> {
|
||||
fn id(&self) -> usize {
|
||||
self.id()
|
||||
}
|
||||
|
||||
fn to_any(&self) -> AnyViewHandle {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<AnyViewHandle> for &dyn NotificationHandle {
|
||||
fn into(self) -> AnyViewHandle {
|
||||
self.to_any()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WorkspaceParams {
|
||||
pub project: ModelHandle<Project>,
|
||||
@ -683,6 +708,7 @@ pub struct Workspace {
|
||||
panes: Vec<ViewHandle<Pane>>,
|
||||
active_pane: ViewHandle<Pane>,
|
||||
status_bar: ViewHandle<StatusBar>,
|
||||
notifications: Vec<Box<dyn NotificationHandle>>,
|
||||
project: ModelHandle<Project>,
|
||||
leader_state: LeaderState,
|
||||
follower_states_by_leader: FollowerStatesByLeader,
|
||||
@ -791,6 +817,7 @@ impl Workspace {
|
||||
panes: vec![pane.clone()],
|
||||
active_pane: pane.clone(),
|
||||
status_bar,
|
||||
notifications: Default::default(),
|
||||
client: params.client.clone(),
|
||||
remote_entity_subscription: None,
|
||||
user_store: params.user_store.clone(),
|
||||
@ -943,7 +970,7 @@ impl Workspace {
|
||||
) -> Option<ViewHandle<V>>
|
||||
where
|
||||
V: 'static + View,
|
||||
F: FnOnce(&mut ViewContext<Self>, &mut Self) -> ViewHandle<V>,
|
||||
F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
|
||||
{
|
||||
cx.notify();
|
||||
// Whatever modal was visible is getting clobbered. If its the same type as V, then return
|
||||
@ -953,7 +980,7 @@ impl Workspace {
|
||||
cx.focus_self();
|
||||
Some(already_open_modal)
|
||||
} else {
|
||||
let modal = add_view(cx, self);
|
||||
let modal = add_view(self, cx);
|
||||
cx.focus(&modal);
|
||||
self.modal = Some(modal.into());
|
||||
None
|
||||
@ -971,6 +998,32 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_notification<V: Notification>(
|
||||
&mut self,
|
||||
notification: ViewHandle<V>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
cx.subscribe(¬ification, |this, handle, event, cx| {
|
||||
if handle.read(cx).should_dismiss_notification_on_event(event) {
|
||||
this.dismiss_notification(handle.id(), cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
self.notifications.push(Box::new(notification));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn dismiss_notification(&mut self, id: usize, cx: &mut ViewContext<Self>) {
|
||||
self.notifications.retain(|handle| {
|
||||
if handle.id() == id {
|
||||
cx.notify();
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn items<'a>(
|
||||
&'a self,
|
||||
cx: &'a AppContext,
|
||||
@ -1049,7 +1102,7 @@ impl Workspace {
|
||||
};
|
||||
let active_item = sidebar.update(cx, |sidebar, cx| {
|
||||
sidebar.toggle_item(action.item_index, cx);
|
||||
sidebar.active_item().cloned()
|
||||
sidebar.active_item().map(|item| item.to_any())
|
||||
});
|
||||
if let Some(active_item) = active_item {
|
||||
cx.focus(active_item);
|
||||
@ -1070,7 +1123,7 @@ impl Workspace {
|
||||
};
|
||||
let active_item = sidebar.update(cx, |sidebar, cx| {
|
||||
sidebar.activate_item(action.item_index, cx);
|
||||
sidebar.active_item().cloned()
|
||||
sidebar.active_item().map(|item| item.to_any())
|
||||
});
|
||||
if let Some(active_item) = active_item {
|
||||
if active_item.is_focused(cx) {
|
||||
@ -1703,6 +1756,30 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_notifications(&self, theme: &theme::Workspace) -> Option<ElementBox> {
|
||||
if self.notifications.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Flex::column()
|
||||
.with_children(self.notifications.iter().map(|notification| {
|
||||
ChildView::new(notification.as_ref())
|
||||
.contained()
|
||||
.with_style(theme.notification)
|
||||
.boxed()
|
||||
}))
|
||||
.constrained()
|
||||
.with_width(theme.notifications.width)
|
||||
.contained()
|
||||
.with_style(theme.notifications.container)
|
||||
.aligned()
|
||||
.bottom()
|
||||
.right()
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// RPC handlers
|
||||
|
||||
async fn handle_follow(
|
||||
@ -2037,6 +2114,7 @@ impl View for Workspace {
|
||||
.top()
|
||||
.boxed()
|
||||
}))
|
||||
.with_children(self.render_notifications(&theme.workspace))
|
||||
.flex(1.0, true)
|
||||
.boxed(),
|
||||
)
|
||||
|
@ -172,7 +172,8 @@ pub fn build_workspace(
|
||||
});
|
||||
|
||||
let project_panel = ProjectPanel::new(project, cx);
|
||||
let contact_panel = cx.add_view(|cx| ContactsPanel::new(app_state.clone(), cx));
|
||||
let contact_panel =
|
||||
cx.add_view(|cx| ContactsPanel::new(app_state.clone(), workspace.weak_handle(), cx));
|
||||
|
||||
workspace.left_sidebar().update(cx, |sidebar, cx| {
|
||||
sidebar.add_item("icons/folder-tree-solid-14.svg", project_panel.into(), cx)
|
||||
|
5
styles/dist/dark.json
vendored
5
styles/dist/dark.json
vendored
@ -332,6 +332,11 @@
|
||||
"description": "Step: 900",
|
||||
"type": "color"
|
||||
},
|
||||
"onMedia": {
|
||||
"value": "#0707071a",
|
||||
"description": "Step: 875",
|
||||
"type": "color"
|
||||
},
|
||||
"ok": {
|
||||
"value": "#1b944726",
|
||||
"description": "Step: 600",
|
||||
|
5
styles/dist/light.json
vendored
5
styles/dist/light.json
vendored
@ -332,6 +332,11 @@
|
||||
"description": "Step: 250",
|
||||
"type": "color"
|
||||
},
|
||||
"onMedia": {
|
||||
"value": "#b8b8b84d",
|
||||
"description": "Step: 250",
|
||||
"type": "color"
|
||||
},
|
||||
"ok": {
|
||||
"value": "#1b944726",
|
||||
"description": "Step: 600",
|
||||
|
4
styles/dist/solarized-dark.json
vendored
4
styles/dist/solarized-dark.json
vendored
@ -271,6 +271,10 @@
|
||||
"value": "#657b83",
|
||||
"type": "color"
|
||||
},
|
||||
"onMedia": {
|
||||
"value": "#002b361a",
|
||||
"type": "color"
|
||||
},
|
||||
"ok": {
|
||||
"value": "#85990026",
|
||||
"type": "color"
|
||||
|
4
styles/dist/solarized-light.json
vendored
4
styles/dist/solarized-light.json
vendored
@ -271,6 +271,10 @@
|
||||
"value": "#839496",
|
||||
"type": "color"
|
||||
},
|
||||
"onMedia": {
|
||||
"value": "#fdf6e31a",
|
||||
"type": "color"
|
||||
},
|
||||
"ok": {
|
||||
"value": "#85990026",
|
||||
"type": "color"
|
||||
|
18
styles/dist/tokens.json
vendored
18
styles/dist/tokens.json
vendored
@ -1514,6 +1514,11 @@
|
||||
"description": "Step: 900",
|
||||
"type": "color"
|
||||
},
|
||||
"onMedia": {
|
||||
"value": "#0707071a",
|
||||
"description": "Step: 875",
|
||||
"type": "color"
|
||||
},
|
||||
"ok": {
|
||||
"value": "#1b944726",
|
||||
"description": "Step: 600",
|
||||
@ -2207,6 +2212,11 @@
|
||||
"description": "Step: 250",
|
||||
"type": "color"
|
||||
},
|
||||
"onMedia": {
|
||||
"value": "#b8b8b84d",
|
||||
"description": "Step: 250",
|
||||
"type": "color"
|
||||
},
|
||||
"ok": {
|
||||
"value": "#1b944726",
|
||||
"description": "Step: 600",
|
||||
@ -2839,6 +2849,10 @@
|
||||
"value": "#657b83",
|
||||
"type": "color"
|
||||
},
|
||||
"onMedia": {
|
||||
"value": "#002b361a",
|
||||
"type": "color"
|
||||
},
|
||||
"ok": {
|
||||
"value": "#85990026",
|
||||
"type": "color"
|
||||
@ -3406,6 +3420,10 @@
|
||||
"value": "#839496",
|
||||
"type": "color"
|
||||
},
|
||||
"onMedia": {
|
||||
"value": "#fdf6e31a",
|
||||
"type": "color"
|
||||
},
|
||||
"ok": {
|
||||
"value": "#85990026",
|
||||
"type": "color"
|
||||
|
@ -10,6 +10,7 @@ import search from "./search";
|
||||
import picker from "./picker";
|
||||
import workspace from "./workspace";
|
||||
import projectDiagnostics from "./projectDiagnostics";
|
||||
import contactNotification from "./contactNotification";
|
||||
|
||||
export const panel = {
|
||||
padding: { top: 12, bottom: 12 },
|
||||
@ -32,6 +33,7 @@ export default function app(theme: Theme): Object {
|
||||
padding: {
|
||||
left: 6,
|
||||
},
|
||||
}
|
||||
},
|
||||
contactNotification: contactNotification(theme),
|
||||
};
|
||||
}
|
||||
|
44
styles/src/styleTree/contactNotification.ts
Normal file
44
styles/src/styleTree/contactNotification.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import Theme from "../themes/theme";
|
||||
import { backgroundColor, iconColor, text } from "./components";
|
||||
|
||||
const avatarSize = 12;
|
||||
const headerPadding = 8;
|
||||
|
||||
export default function contactNotification(theme: Theme): Object {
|
||||
return {
|
||||
headerAvatar: {
|
||||
height: avatarSize,
|
||||
width: avatarSize,
|
||||
cornerRadius: 6,
|
||||
},
|
||||
headerMessage: {
|
||||
...text(theme, "sans", "primary", { size: "xs" }),
|
||||
margin: { left: headerPadding, right: headerPadding }
|
||||
},
|
||||
headerHeight: 18,
|
||||
bodyMessage: {
|
||||
...text(theme, "sans", "secondary", { size: "xs" }),
|
||||
margin: { left: avatarSize + headerPadding, top: 6, bottom: 6 },
|
||||
},
|
||||
button: {
|
||||
...text(theme, "sans", "primary", { size: "xs" }),
|
||||
background: backgroundColor(theme, "on300"),
|
||||
padding: 4,
|
||||
cornerRadius: 6,
|
||||
margin: { left: 6 },
|
||||
hover: {
|
||||
background: backgroundColor(theme, "on300", "hovered")
|
||||
}
|
||||
},
|
||||
dismissButton: {
|
||||
color: iconColor(theme, "secondary"),
|
||||
iconWidth: 8,
|
||||
iconHeight: 8,
|
||||
buttonWidth: 8,
|
||||
buttonHeight: 8,
|
||||
hover: {
|
||||
color: iconColor(theme, "primary")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import Theme from "../themes/theme";
|
||||
import { backgroundColor, border, iconColor, text } from "./components";
|
||||
import { workspaceBackground } from "./workspace";
|
||||
|
||||
export default function statusBar(theme: Theme) {
|
||||
|
||||
const statusContainer = {
|
||||
cornerRadius: 6,
|
||||
padding: { top: 3, bottom: 3, left: 6, right: 6 }
|
||||
@ -100,6 +100,13 @@ export default function statusBar(theme: Theme) {
|
||||
iconColor: iconColor(theme, "active"),
|
||||
background: backgroundColor(theme, 300, "active"),
|
||||
}
|
||||
},
|
||||
badge: {
|
||||
cornerRadius: 3,
|
||||
padding: 2,
|
||||
margin: { bottom: -1, right: -1 },
|
||||
border: { width: 1, color: workspaceBackground(theme) },
|
||||
background: iconColor(theme, "feature"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,16 @@
|
||||
import Theme from "../themes/theme";
|
||||
import { backgroundColor, border, iconColor, text } from "./components";
|
||||
import { backgroundColor, border, iconColor, shadow, text } from "./components";
|
||||
import statusBar from "./statusBar";
|
||||
|
||||
export function workspaceBackground(theme: Theme) {
|
||||
return backgroundColor(theme, 300)
|
||||
}
|
||||
|
||||
export default function workspace(theme: Theme) {
|
||||
|
||||
const tab = {
|
||||
height: 32,
|
||||
background: backgroundColor(theme, 300),
|
||||
background: workspaceBackground(theme),
|
||||
iconClose: iconColor(theme, "muted"),
|
||||
iconCloseActive: iconColor(theme, "active"),
|
||||
iconConflict: iconColor(theme, "warning"),
|
||||
@ -146,5 +150,17 @@ export default function workspace(theme: Theme) {
|
||||
...text(theme, "sans", "active"),
|
||||
background: "#000000aa",
|
||||
},
|
||||
notification: {
|
||||
margin: { top: 10 },
|
||||
background: backgroundColor(theme, 300),
|
||||
cornerRadius: 6,
|
||||
padding: 12,
|
||||
border: border(theme, "primary"),
|
||||
shadow: shadow(theme),
|
||||
},
|
||||
notifications: {
|
||||
width: 380,
|
||||
margin: { right: 10, bottom: 10 },
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -82,6 +82,7 @@ export function createTheme(name: string, isLight: boolean, neutral: ColorToken[
|
||||
muted: neutral[3],
|
||||
focused: neutral[3],
|
||||
active: neutral[3],
|
||||
onMedia: withOpacity(neutral[0], 0.1),
|
||||
ok: withOpacity(accent.green, 0.15),
|
||||
error: withOpacity(accent.red, 0.15),
|
||||
warning: withOpacity(accent.yellow, 0.15),
|
||||
|
@ -65,6 +65,7 @@ const borderColor = {
|
||||
muted: colors.neutral[675],
|
||||
focused: colors.indigo[500],
|
||||
active: colors.neutral[900],
|
||||
onMedia: withOpacity(colors.neutral[875], 0.1),
|
||||
ok: withOpacity(colors.green[600], 0.15),
|
||||
error: withOpacity(colors.red[500], 0.15),
|
||||
warning: withOpacity(colors.amber[400], 0.15),
|
||||
|
@ -65,6 +65,7 @@ const borderColor = {
|
||||
muted: colors.neutral[100],
|
||||
focused: colors.indigo[500],
|
||||
active: colors.neutral[250],
|
||||
onMedia: withOpacity(colors.neutral[250], 0.3),
|
||||
ok: withOpacity(colors.green[600], 0.15),
|
||||
error: withOpacity(colors.red[500], 0.15),
|
||||
warning: withOpacity(colors.amber[400], 0.15),
|
||||
|
@ -87,6 +87,10 @@ export default interface Theme {
|
||||
muted: ColorToken;
|
||||
focused: ColorToken;
|
||||
active: ColorToken;
|
||||
/**
|
||||
* Used for rendering borders on top of media like avatars, images, video, etc.
|
||||
*/
|
||||
onMedia: ColorToken;
|
||||
ok: ColorToken;
|
||||
error: ColorToken;
|
||||
warning: ColorToken;
|
||||
|
Loading…
Reference in New Issue
Block a user